@bastani/atomic 0.9.4-alpha.3 → 0.9.4-alpha.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/builtin/cursor/CHANGELOG.md +12 -0
  3. package/dist/builtin/cursor/package.json +2 -2
  4. package/dist/builtin/intercom/CHANGELOG.md +12 -0
  5. package/dist/builtin/intercom/package.json +1 -1
  6. package/dist/builtin/mcp/CHANGELOG.md +12 -0
  7. package/dist/builtin/mcp/package.json +1 -1
  8. package/dist/builtin/subagents/CHANGELOG.md +12 -0
  9. package/dist/builtin/subagents/package.json +1 -1
  10. package/dist/builtin/web-access/CHANGELOG.md +12 -0
  11. package/dist/builtin/web-access/package.json +1 -1
  12. package/dist/builtin/workflows/CHANGELOG.md +12 -0
  13. package/dist/builtin/workflows/package.json +1 -1
  14. package/dist/core/agent-session-auto-compaction.d.ts.map +1 -1
  15. package/dist/core/agent-session-auto-compaction.js +8 -1
  16. package/dist/core/agent-session-auto-compaction.js.map +1 -1
  17. package/dist/core/agent-session-methods.d.ts +1 -0
  18. package/dist/core/agent-session-methods.d.ts.map +1 -1
  19. package/dist/core/agent-session-methods.js.map +1 -1
  20. package/dist/core/agent-session-prompt.d.ts +2 -0
  21. package/dist/core/agent-session-prompt.d.ts.map +1 -1
  22. package/dist/core/agent-session-prompt.js +6 -0
  23. package/dist/core/agent-session-prompt.js.map +1 -1
  24. package/dist/core/agent-session-types.d.ts +4 -0
  25. package/dist/core/agent-session-types.d.ts.map +1 -1
  26. package/dist/core/agent-session-types.js.map +1 -1
  27. package/dist/core/copilot-model-catalog.d.ts +18 -5
  28. package/dist/core/copilot-model-catalog.d.ts.map +1 -1
  29. package/dist/core/copilot-model-catalog.js +64 -5
  30. package/dist/core/copilot-model-catalog.js.map +1 -1
  31. package/dist/core/http-dispatcher.d.ts +2 -0
  32. package/dist/core/http-dispatcher.d.ts.map +1 -1
  33. package/dist/core/http-dispatcher.js +11 -5
  34. package/dist/core/http-dispatcher.js.map +1 -1
  35. package/dist/core/model-registry-builtins.d.ts.map +1 -1
  36. package/dist/core/model-registry-builtins.js +10 -2
  37. package/dist/core/model-registry-builtins.js.map +1 -1
  38. package/dist/core/model-registry.d.ts.map +1 -1
  39. package/dist/core/model-registry.js +4 -3
  40. package/dist/core/model-registry.js.map +1 -1
  41. package/dist/modes/interactive/components/chat-session-host-events.d.ts.map +1 -1
  42. package/dist/modes/interactive/components/chat-session-host-events.js +8 -0
  43. package/dist/modes/interactive/components/chat-session-host-events.js.map +1 -1
  44. package/dist/modes/interactive/interactive-agent-events.d.ts.map +1 -1
  45. package/dist/modes/interactive/interactive-agent-events.js +5 -0
  46. package/dist/modes/interactive/interactive-agent-events.js.map +1 -1
  47. package/dist/modes/interactive/interactive-model-routing.d.ts.map +1 -1
  48. package/dist/modes/interactive/interactive-model-routing.js +1 -4
  49. package/dist/modes/interactive/interactive-model-routing.js.map +1 -1
  50. package/docs/compaction.md +2 -0
  51. package/docs/providers.md +1 -0
  52. package/npm-shrinkwrap.json +23 -23
  53. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"copilot-model-catalog.d.ts","sourceRoot":"","sources":["../../src/core/copilot-model-catalog.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * GitHub Copilot model catalog (CAPI) — dynamic prompt-token budgets.\n *\n * GitHub's Copilot API (CAPI) exposes distinct model limits via `GET {baseUrl}/models`:\n *\n * - `capabilities.limits.max_context_window_tokens` is the model's total context capacity\n * (prompt + completion reserve).\n * - `capabilities.limits.max_prompt_tokens` is the maximum prompt/input budget Atomic can safely\n * fill before the provider must reserve output tokens.\n * - `billing.token_prices.<tier>.context_max` is a prompt-token billing/selection threshold. The\n * `default` tier is the short prompt budget (e.g. gpt-5.5 272k, Claude 200k); a\n * `long_context` tier adds a selectable larger prompt budget (e.g. gpt-5.5 922k, Claude 936k).\n *\n * Atomic shows the model's full context window for the selectable long tier (the\n * `max_context_window_tokens` total, e.g. 1_000_000/1_050_000), matching how the native `openai/*`\n * and `anthropic/*` providers advertise these models. Because GitHub enforces a lower server-side\n * prompt cap (`max_prompt_tokens`, e.g. 936k/922k) below that total, the prompt cap is retained as\n * an internal effective input budget (`CopilotModelContext.maxInputTokens`) that drives compaction\n * thresholds and the overflow-recovery guard, so the branded total can be displayed without\n * overrunning the server limit. The default (short) tier stays at the `default` billing tier's\n * prompt budget.\n *\n * This data is intentionally NOT baked into a static map: GitHub adds/removes models and retiers\n * windows over time (e.g. a model that disappears from the catalog), so a hardcoded snapshot goes\n * stale. Instead the catalog is fetched live (gated on the user actually having the GitHub Copilot\n * provider) and cached on disk for a short TTL, exactly like the Copilot CLI.\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\n\n/** Resolved input-token context window(s) for a single Copilot model. */\nexport interface CopilotModelContext {\n\t/**\n\t * Base/displayed context window — shown in the footer. The default tier's `context_max`, or the\n\t * model-level `max_prompt_tokens` fallback otherwise.\n\t */\n\tcontextWindow: number;\n\t/**\n\t * Selectable windows (`[default, long]`) when the model exposes a `long_context` tier larger than\n\t * its default; absent for single-window models. The long entry is the model's full\n\t * `max_context_window_tokens` (total capacity) when advertised, matching `openai/*` and\n\t * `anthropic/*`.\n\t */\n\tcontextWindowOptions?: readonly number[];\n\t/**\n\t * Hard prompt/input cap (`max_prompt_tokens`) when it sits below the displayed long window. Used\n\t * as the effective input budget for compaction thresholds and overflow recovery so the branded\n\t * total can be shown without overrunning GitHub's server-side prompt limit. Absent when the\n\t * displayed window already equals the input cap.\n\t */\n\tmaxInputTokens?: number;\n}\n\n/** Map of model id → resolved input-token context window(s). */\nexport type CopilotModelCatalog = ReadonlyMap<string, CopilotModelContext>;\n\n/** Safety fallback when a model reports neither `max_prompt_tokens` nor `max_context_window_tokens`. */\nexport const COPILOT_CONTEXT_WINDOW_FALLBACK = 128_000;\n\nexport const COPILOT_CATALOG_API_VERSION = \"2026-06-01\";\n\n/**\n * Headers GitHub's CAPI expects for catalog reads. Mirrors the editor headers pi-ai already sends\n * for Copilot token refresh and model-policy calls, plus the dated API version.\n */\nexport const COPILOT_CATALOG_HEADERS: Readonly<Record<string, string>> = {\n\t\"User-Agent\": \"GitHubCopilotChat/0.35.0\",\n\t\"Editor-Version\": \"vscode/1.107.0\",\n\t\"Editor-Plugin-Version\": \"copilot-chat/0.35.0\",\n\t\"Copilot-Integration-Id\": \"vscode-chat\",\n\t\"X-GitHub-Api-Version\": COPILOT_CATALOG_API_VERSION,\n};\n\n/** Default (non-enterprise) Copilot CAPI base URL when the token has no resolvable `proxy-ep`. */\nexport const DEFAULT_COPILOT_API_BASE_URL = \"https://api.individual.githubcopilot.com\";\n\n/** Disk-cache freshness window, matching the Copilot CLI's list-models cache TTL. */\nexport const COPILOT_CATALOG_CACHE_TTL_MS = 30 * 60 * 1000;\n\n/** Current on-disk cache schema version. */\nexport const COPILOT_CATALOG_CACHE_VERSION = 3 as const;\n\n/**\n * Resolve the Copilot CAPI base URL.\n *\n * Copilot access tokens embed a `proxy-ep=proxy.<host>` segment; the API host is the same host with\n * `proxy.` swapped for `api.`. Falls back to the enterprise host or the individual default. (pi-ai\n * exposes an equivalent helper, but its published `dist` mangles the export name, so the small,\n * stable parsing logic is reimplemented here.)\n */\nexport function copilotApiBaseUrlFromToken(token: string | undefined, enterpriseDomain?: string): string {\n\tif (token) {\n\t\tconst match = token.match(/proxy-ep=([^;]+)/);\n\t\tif (match) {\n\t\t\treturn `https://${match[1].replace(/^proxy\\./, \"api.\")}`;\n\t\t}\n\t}\n\tif (enterpriseDomain) return `https://copilot-api.${enterpriseDomain}`;\n\treturn DEFAULT_COPILOT_API_BASE_URL;\n}\n\nfunction trimTrailingSlash(url: string): string {\n\treturn url.replace(/\\/+$/, \"\");\n}\n\nfunction asRecord(value: unknown): Record<string, unknown> | undefined {\n\treturn value && typeof value === \"object\" ? (value as Record<string, unknown>) : undefined;\n}\n\nfunction toPositiveInt(value: unknown): number | undefined {\n\treturn typeof value === \"number\" && Number.isInteger(value) && value > 0 ? value : undefined;\n}\n\n/** Raw token limits parsed from a CAPI model entry. */\nexport interface CopilotModelLimits {\n\t/** `capabilities.limits.max_prompt_tokens` — maximum prompt/input budget (the hard input cap). */\n\tmaxPromptTokens?: number;\n\t/** `capabilities.limits.max_context_window_tokens` — total context capacity (the displayed long tier). */\n\tmaxContextWindowTokens?: number;\n\t/** `capabilities.limits.max_output_tokens` — output reserve; derives the input cap when `max_prompt_tokens` is absent. */\n\tmaxOutputTokens?: number;\n\t/** `billing.token_prices.default.context_max` — default-tier prompt threshold. */\n\tdefaultContextMax?: number;\n\t/** `billing.token_prices.long_context.context_max` — long-context prompt threshold. */\n\tlongContextMax?: number;\n}\n\n/**\n * Resolve a model's input-token context window(s) from its CAPI limits.\n *\n * `contextWindow` is the model's base input budget — the default tier's `context_max` when tiered,\n * otherwise `max_prompt_tokens ?? max_context_window_tokens ?? 128_000`. A `long_context` tier that\n * is larger than the base adds a second selectable window. Returns `undefined` when the entry\n * carries no usable limit signal at all.\n */\nexport function resolveCopilotModelContext(limits: CopilotModelLimits): CopilotModelContext | undefined {\n\tconst hasSignal =\n\t\tlimits.maxPromptTokens !== undefined ||\n\t\tlimits.maxContextWindowTokens !== undefined ||\n\t\tlimits.defaultContextMax !== undefined ||\n\t\tlimits.longContextMax !== undefined;\n\tif (!hasSignal) return undefined;\n\n\tconst maxInput = limits.maxPromptTokens ?? limits.maxContextWindowTokens ?? COPILOT_CONTEXT_WINDOW_FALLBACK;\n\tconst base = limits.defaultContextMax ?? maxInput;\n\tif (limits.longContextMax !== undefined && limits.longContextMax > base) {\n\t\t// Display the model's full context window as the long tier (matching openai/* and anthropic/*)\n\t\t// when CAPI advertises it; otherwise fall back to the long-context prompt threshold for\n\t\t// older/sparse payloads.\n\t\tconst longWindow = limits.maxContextWindowTokens ?? limits.longContextMax;\n\t\t// The hard prompt/input cap GitHub enforces server-side: prefer max_prompt_tokens, else derive\n\t\t// it from total − output reserve, else fall back to the long-context prompt threshold.\n\t\tconst derivedInputCap =\n\t\t\tlimits.maxContextWindowTokens !== undefined && limits.maxOutputTokens !== undefined\n\t\t\t\t? limits.maxContextWindowTokens - limits.maxOutputTokens\n\t\t\t\t: undefined;\n\t\tconst inputCap =\n\t\t\tlimits.maxPromptTokens ??\n\t\t\t(derivedInputCap !== undefined && derivedInputCap > 0 ? derivedInputCap : undefined) ??\n\t\t\tlimits.longContextMax;\n\t\t// Only carry the cap when the displayed long window actually exceeds it (the branded-total\n\t\t// case); when they coincide there is no gap and the input budget is just the window.\n\t\treturn longWindow > inputCap\n\t\t\t? { contextWindow: base, contextWindowOptions: [base, longWindow], maxInputTokens: inputCap }\n\t\t\t: { contextWindow: base, contextWindowOptions: [base, longWindow] };\n\t}\n\treturn { contextWindow: base };\n}\n\n/**\n * Parse a raw CAPI `/models` response body into an input-token context-window catalog.\n */\nexport function parseCopilotModelCatalog(body: unknown): CopilotModelCatalog {\n\tconst catalog = new Map<string, CopilotModelContext>();\n\tconst data = asRecord(body)?.data;\n\tif (!Array.isArray(data)) return catalog;\n\n\tfor (const entry of data) {\n\t\tconst record = asRecord(entry);\n\t\tif (!record) continue;\n\t\tconst id = record.id;\n\t\tif (typeof id !== \"string\" || id.length === 0) continue;\n\n\t\tconst limits = asRecord(asRecord(record.capabilities)?.limits);\n\t\tconst prices = asRecord(asRecord(record.billing)?.token_prices);\n\t\tconst context = resolveCopilotModelContext({\n\t\t\tmaxPromptTokens: toPositiveInt(limits?.max_prompt_tokens),\n\t\t\tmaxContextWindowTokens: toPositiveInt(limits?.max_context_window_tokens),\n\t\t\tmaxOutputTokens: toPositiveInt(limits?.max_output_tokens),\n\t\t\tdefaultContextMax: toPositiveInt(asRecord(prices?.default)?.context_max),\n\t\t\tlongContextMax: toPositiveInt(asRecord(prices?.long_context)?.context_max),\n\t\t});\n\t\tif (context) catalog.set(id, context);\n\t}\n\n\treturn catalog;\n}\n\nexport interface FetchCopilotModelCatalogOptions {\n\t/** Valid Copilot CAPI bearer token (e.g. from `modelRegistry.getApiKeyForProvider`). */\n\ttoken: string;\n\t/** Override the resolved base URL; defaults to one derived from the token. */\n\tbaseUrl?: string;\n\t/** Enterprise domain, used for base-URL resolution when the token lacks a `proxy-ep`. */\n\tenterpriseDomain?: string;\n\t/** Extra/override request headers. */\n\theaders?: Record<string, string>;\n\t/** Injectable `fetch` for testing. */\n\tfetchImpl?: typeof fetch;\n\t/** Abort signal. */\n\tsignal?: AbortSignal;\n}\n\n/** Fetch and parse the live Copilot model catalog from CAPI `GET {baseUrl}/models`. */\nexport async function fetchCopilotModelCatalog(options: FetchCopilotModelCatalogOptions): Promise<CopilotModelCatalog> {\n\tconst fetchImpl = options.fetchImpl ?? fetch;\n\tconst baseUrl = options.baseUrl ?? copilotApiBaseUrlFromToken(options.token, options.enterpriseDomain);\n\tconst response = await fetchImpl(`${trimTrailingSlash(baseUrl)}/models`, {\n\t\tmethod: \"GET\",\n\t\theaders: {\n\t\t\tAccept: \"application/json\",\n\t\t\tAuthorization: `Bearer ${options.token}`,\n\t\t\t...COPILOT_CATALOG_HEADERS,\n\t\t\t...options.headers,\n\t\t},\n\t\t...(options.signal ? { signal: options.signal } : {}),\n\t});\n\tif (!response.ok) {\n\t\tthrow new Error(`GitHub Copilot /models request failed: ${response.status} ${response.statusText}`);\n\t}\n\treturn parseCopilotModelCatalog(await response.json());\n}\n\n// ----------------------------------------------------------------------------\n// Active in-memory catalog (consulted by the model registry).\n//\n// Empty by default, so with no Copilot auth / no successful fetch the registry leaves Copilot\n// model context windows untouched and the picker never appears.\n// ----------------------------------------------------------------------------\n\nlet activeCatalog: CopilotModelCatalog = new Map();\n\n/** Replace the active catalog the registry derives context windows from. */\nexport function setActiveCopilotModelCatalog(catalog: CopilotModelCatalog): void {\n\tactiveCatalog = catalog;\n}\n\n/** The active catalog (empty until a successful auth-gated fetch/cache load). */\nexport function getActiveCopilotModelCatalog(): CopilotModelCatalog {\n\treturn activeCatalog;\n}\n\n/** Reset the active catalog (primarily for tests). */\nexport function clearActiveCopilotModelCatalog(): void {\n\tactiveCatalog = new Map();\n}\n\n// ----------------------------------------------------------------------------\n// Disk cache.\n// ----------------------------------------------------------------------------\n\ninterface CopilotCatalogCacheFile {\n\tversion: typeof COPILOT_CATALOG_CACHE_VERSION;\n\t/** CAPI host the catalog was fetched from; cache misses on host change (e.g. enterprise switch). */\n\thost: string;\n\t/** Epoch ms the catalog was fetched. */\n\tfetchedAt: number;\n\tmodels: Record<string, CopilotModelContext>;\n}\n\nfunction hostFromBaseUrl(baseUrl: string): string {\n\ttry {\n\t\treturn new URL(baseUrl).host;\n\t} catch {\n\t\treturn baseUrl;\n\t}\n}\n\nexport interface ReadCopilotCatalogCacheOptions {\n\t/** Expected CAPI host; a cached file from a different host is ignored. */\n\thost: string;\n\t/** Current epoch ms (injectable for tests). */\n\tnow?: number;\n\t/** Freshness window; defaults to {@link COPILOT_CATALOG_CACHE_TTL_MS}. */\n\tttlMs?: number;\n}\n\nfunction sanitizeCachedContext(value: unknown): CopilotModelContext | undefined {\n\tconst record = asRecord(value);\n\tconst contextWindow = toPositiveInt(record?.contextWindow);\n\tif (contextWindow === undefined) return undefined;\n\tconst maxInputTokens = toPositiveInt(record?.maxInputTokens);\n\tconst rawOptions = record?.contextWindowOptions;\n\tif (Array.isArray(rawOptions)) {\n\t\tconst options = rawOptions.map(toPositiveInt).filter((n): n is number => n !== undefined);\n\t\tif (options.length > 1) {\n\t\t\treturn maxInputTokens !== undefined\n\t\t\t\t? { contextWindow, contextWindowOptions: options, maxInputTokens }\n\t\t\t\t: { contextWindow, contextWindowOptions: options };\n\t\t}\n\t}\n\treturn maxInputTokens !== undefined ? { contextWindow, maxInputTokens } : { contextWindow };\n}\n\n/** Read a fresh, host-matching catalog from the cache file, or `undefined` if missing/stale/invalid. */\nexport function readCopilotCatalogCache(\n\tpath: string,\n\toptions: ReadCopilotCatalogCacheOptions,\n): CopilotModelCatalog | undefined {\n\tlet parsed: CopilotCatalogCacheFile;\n\ttry {\n\t\tif (!existsSync(path)) return undefined;\n\t\tparsed = JSON.parse(readFileSync(path, \"utf8\")) as CopilotCatalogCacheFile;\n\t} catch {\n\t\treturn undefined;\n\t}\n\tif (!parsed || parsed.version !== COPILOT_CATALOG_CACHE_VERSION) return undefined;\n\tif (parsed.host !== options.host) return undefined;\n\tconst now = options.now ?? Date.now();\n\tconst ttlMs = options.ttlMs ?? COPILOT_CATALOG_CACHE_TTL_MS;\n\tif (typeof parsed.fetchedAt !== \"number\" || now - parsed.fetchedAt >= ttlMs) return undefined;\n\tconst models = asRecord(parsed.models);\n\tif (!models) return undefined;\n\n\tconst catalog = new Map<string, CopilotModelContext>();\n\tfor (const [id, value] of Object.entries(models)) {\n\t\tconst context = sanitizeCachedContext(value);\n\t\tif (context) catalog.set(id, context);\n\t}\n\treturn catalog;\n}\n\n/** Write the catalog to the cache file (creating parent dirs). Best-effort; never throws. */\nexport function writeCopilotCatalogCache(\n\tpath: string,\n\tbaseUrl: string,\n\tcatalog: CopilotModelCatalog,\n\tnow?: number,\n): void {\n\tconst payload: CopilotCatalogCacheFile = {\n\t\tversion: COPILOT_CATALOG_CACHE_VERSION,\n\t\thost: hostFromBaseUrl(baseUrl),\n\t\tfetchedAt: now ?? Date.now(),\n\t\tmodels: Object.fromEntries(catalog),\n\t};\n\ttry {\n\t\tmkdirSync(dirname(path), { recursive: true });\n\t\twriteFileSync(path, JSON.stringify(payload), \"utf8\");\n\t} catch {\n\t\t// best-effort cache; ignore write failures\n\t}\n}\n\n/** Host component of a base URL, for matching {@link readCopilotCatalogCache} `host`. */\nexport function copilotCatalogCacheHost(baseUrl: string): string {\n\treturn hostFromBaseUrl(baseUrl);\n}\n\n/** Standard on-disk cache path for the Copilot model catalog under an agent directory. */\nexport function copilotCatalogCachePath(agentDir: string): string {\n\treturn join(agentDir, \"cache\", \"copilot-models.json\");\n}\n\n/**\n * Seed the active catalog synchronously from the on-disk cache, gated on a Copilot access token.\n *\n * Called at model-registry construction so a returning user's previously selected long-context\n * window is recognized before startup validation runs — otherwise the persisted choice would warn\n * (\"context window 936k is not supported…\") and reset until the async refresh completes. The cache\n * TTL is intentionally ignored here: stale-but-present windows are still valid for selection, and\n * the async loader independently refetches on its own freshness window. Returns true when a catalog\n * was applied. No-op (returns false) without a token or a host-matching cached catalog.\n */\nexport function seedActiveCopilotModelCatalogFromCache(\n\taccessToken: string | undefined,\n\tcachePath: string,\n\tnow?: number,\n): boolean {\n\tif (typeof accessToken !== \"string\" || accessToken.length === 0) return false;\n\tconst host = copilotCatalogCacheHost(copilotApiBaseUrlFromToken(accessToken));\n\tconst cached = readCopilotCatalogCache(cachePath, { host, now, ttlMs: Number.POSITIVE_INFINITY });\n\tif (!cached) return false;\n\tsetActiveCopilotModelCatalog(cached);\n\treturn true;\n}\n"]}
1
+ {"version":3,"file":"copilot-model-catalog.d.ts","sourceRoot":"","sources":["../../src/core/copilot-model-catalog.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * GitHub Copilot model catalog (CAPI) — dynamic prompt-token budgets.\n *\n * GitHub's Copilot API (CAPI) exposes distinct model limits via `GET {baseUrl}/models`:\n *\n * - `capabilities.limits.max_context_window_tokens` is the model's total context capacity\n * (prompt + completion reserve).\n * - `capabilities.limits.max_prompt_tokens` is the maximum prompt/input budget Atomic can safely\n * fill before the provider must reserve output tokens.\n * - `billing.token_prices.<tier>.context_max` is a prompt-token billing/selection threshold. The\n * `default` tier is the short prompt budget (e.g. gpt-5.5 272k, Claude 200k); a\n * `long_context` tier adds a selectable larger prompt budget (e.g. gpt-5.5 922k, Claude 936k).\n *\n * Atomic shows the model's full context window for the selectable long tier (the\n * `max_context_window_tokens` total, e.g. 1_000_000/1_050_000), matching how the native `openai/*`\n * and `anthropic/*` providers advertise these models. Because GitHub enforces a lower server-side\n * prompt cap (`max_prompt_tokens`, e.g. 936k/922k) below that total, the prompt cap is retained as\n * an internal effective input budget (`CopilotModelContext.maxInputTokens`) that drives compaction\n * thresholds and the overflow-recovery guard, so the branded total can be displayed without\n * overrunning the server limit. The default (short) tier stays at the `default` billing tier's\n * prompt budget.\n *\n * This data is intentionally NOT baked into a static map: GitHub adds/removes models and retiers\n * windows over time (e.g. a model that disappears from the catalog), so a hardcoded snapshot goes\n * stale. Instead the catalog is fetched live (gated on the user actually having the GitHub Copilot\n * provider) and cached on disk for a short TTL, exactly like the Copilot CLI.\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\n\n/** Resolved input-token context window(s) for a single Copilot model. */\nexport interface CopilotModelContext {\n\t/**\n\t * Base/displayed context window — shown in the footer. The default tier's `context_max`, or the\n\t * model-level `max_prompt_tokens` fallback otherwise.\n\t */\n\tcontextWindow: number;\n\t/**\n\t * Selectable windows (`[default, long]`) when the model exposes a `long_context` tier larger than\n\t * its default; absent for single-window models. The long entry is the model's full\n\t * `max_context_window_tokens` (total capacity) when advertised, matching `openai/*` and\n\t * `anthropic/*`.\n\t */\n\tcontextWindowOptions?: readonly number[];\n\t/**\n\t * Hard prompt/input cap (`max_prompt_tokens`) when it sits below the displayed long window. Used\n\t * as the effective input budget for compaction thresholds and overflow recovery so the branded\n\t * total can be shown without overrunning GitHub's server-side prompt limit. Absent when the\n\t * displayed window already equals the input cap.\n\t */\n\tmaxInputTokens?: number;\n}\n\n/** Map of model id → resolved input-token context window(s). */\nexport type CopilotModelCatalog = ReadonlyMap<string, CopilotModelContext>;\n\n/** Safety fallback when a model reports neither `max_prompt_tokens` nor `max_context_window_tokens`. */\nexport const COPILOT_CONTEXT_WINDOW_FALLBACK = 128_000;\n\nexport const COPILOT_CATALOG_API_VERSION = \"2026-06-01\";\n\n/**\n * Headers GitHub's CAPI expects for catalog reads. Mirrors the editor headers pi-ai already sends\n * for Copilot token refresh and model-policy calls, plus the dated API version.\n */\nexport const COPILOT_CATALOG_HEADERS: Readonly<Record<string, string>> = {\n\t\"User-Agent\": \"GitHubCopilotChat/0.35.0\",\n\t\"Editor-Version\": \"vscode/1.107.0\",\n\t\"Editor-Plugin-Version\": \"copilot-chat/0.35.0\",\n\t\"Copilot-Integration-Id\": \"vscode-chat\",\n\t\"X-GitHub-Api-Version\": COPILOT_CATALOG_API_VERSION,\n};\n\n/** Default Copilot CAPI base URL for OAuth tokens when no route can be resolved. */\nexport const DEFAULT_COPILOT_API_BASE_URL = \"https://api.individual.githubcopilot.com\";\n\n/** GitHub Copilot public routing hub for PAT-based `COPILOT_GITHUB_TOKEN` auth. */\nexport const COPILOT_PUBLIC_API_BASE_URL = \"https://api.githubcopilot.com\";\n\n/** Enterprise Copilot CAPI host for GHES/non-github.com server URLs. */\nexport const COPILOT_ENTERPRISE_API_BASE_URL = \"https://api.enterprise.githubcopilot.com\";\n\nexport const COPILOT_GITHUB_TOKEN_ENV = \"COPILOT_GITHUB_TOKEN\";\nexport const COPILOT_API_TARGET_ENV = \"COPILOT_API_TARGET\";\nexport const GITHUB_COPILOT_BASE_URL_ENV = \"GITHUB_COPILOT_BASE_URL\";\nexport const GITHUB_SERVER_URL_ENV = \"GITHUB_SERVER_URL\";\n\ntype CopilotEnvironment = Record<string, string | undefined>;\n\nfunction normalizeCopilotApiBaseUrl(value: string | undefined): string | undefined {\n\tconst trimmed = value?.trim();\n\tif (!trimmed) return undefined;\n\tconst withScheme = /^https?:\\/\\//i.test(trimmed) ? trimmed : `https://${trimmed}`;\n\ttry {\n\t\tnew URL(withScheme);\n\t} catch {\n\t\treturn undefined;\n\t}\n\treturn withScheme.replace(/\\/+$/, \"\");\n}\n\nfunction explicitCopilotApiBaseUrlFromEnvironment(env: CopilotEnvironment): string | undefined {\n\treturn normalizeCopilotApiBaseUrl(env[COPILOT_API_TARGET_ENV]) ?? normalizeCopilotApiBaseUrl(env[GITHUB_COPILOT_BASE_URL_ENV]);\n}\n\nfunction copilotApiBaseUrlFromServerUrl(serverUrl: string | undefined): string | undefined {\n\tconst trimmed = serverUrl?.trim();\n\tif (!trimmed) return undefined;\n\ttry {\n\t\tconst parsed = new URL(trimmed.includes(\"://\") ? trimmed : `https://${trimmed}`);\n\t\tconst host = parsed.hostname.toLowerCase();\n\t\tif (!host || host === \"github.com\") return undefined;\n\t\tif (host.endsWith(\".ghe.com\")) return `https://copilot-api.${host}`;\n\t\treturn COPILOT_ENTERPRISE_API_BASE_URL;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nfunction isCopilotEnvironmentToken(token: string | undefined, env: CopilotEnvironment): boolean {\n\tconst environmentToken = copilotTokenFromEnvironment(env);\n\treturn environmentToken !== undefined && token === environmentToken;\n}\n\nexport function copilotTokenFromEnvironment(env: CopilotEnvironment = process.env): string | undefined {\n\tconst token = env[COPILOT_GITHUB_TOKEN_ENV]?.trim();\n\treturn token ? token : undefined;\n}\n\n/** Disk-cache freshness window, matching the Copilot CLI's list-models cache TTL. */\nexport const COPILOT_CATALOG_CACHE_TTL_MS = 30 * 60 * 1000;\n\n/** Current on-disk cache schema version. */\nexport const COPILOT_CATALOG_CACHE_VERSION = 3 as const;\n\n/**\n * Resolve the Copilot CAPI base URL.\n *\n * Copilot access tokens embed a `proxy-ep=proxy.<host>` segment; the API host is the same host with\n * `proxy.` swapped for `api.`. Env-token routing resolves explicit `COPILOT_API_TARGET` /\n * `GITHUB_COPILOT_BASE_URL` overrides, then `GITHUB_SERVER_URL` (`*.ghe.com` ->\n * `copilot-api.<tenant>.ghe.com`, other non-github.com -> `api.enterprise.githubcopilot.com`),\n * then the public Copilot routing hub `api.githubcopilot.com` for `COPILOT_GITHUB_TOKEN`. Stored\n * OAuth credentials still fall back to the generated individual host when no token route is known.\n */\nexport function copilotApiBaseUrlFromToken(\n\ttoken: string | undefined,\n\tenterpriseDomain?: string,\n\tenv: CopilotEnvironment = process.env,\n): string {\n\tconst explicitOverride = explicitCopilotApiBaseUrlFromEnvironment(env);\n\tif (explicitOverride) return explicitOverride;\n\tif (token) {\n\t\tconst match = token.match(/proxy-ep=([^;]+)/);\n\t\tif (match) {\n\t\t\treturn `https://${match[1].replace(/^proxy\\./, \"api.\")}`;\n\t\t}\n\t}\n\tif (enterpriseDomain) return `https://copilot-api.${enterpriseDomain}`;\n\tconst serverUrlOverride = copilotApiBaseUrlFromServerUrl(env[GITHUB_SERVER_URL_ENV]);\n\tif (serverUrlOverride) return serverUrlOverride;\n\tif (isCopilotEnvironmentToken(token, env)) return COPILOT_PUBLIC_API_BASE_URL;\n\treturn DEFAULT_COPILOT_API_BASE_URL;\n}\n\nfunction trimTrailingSlash(url: string): string {\n\treturn url.replace(/\\/+$/, \"\");\n}\n\nfunction asRecord(value: unknown): Record<string, unknown> | undefined {\n\treturn value && typeof value === \"object\" ? (value as Record<string, unknown>) : undefined;\n}\n\nfunction toPositiveInt(value: unknown): number | undefined {\n\treturn typeof value === \"number\" && Number.isInteger(value) && value > 0 ? value : undefined;\n}\n\n/** Raw token limits parsed from a CAPI model entry. */\nexport interface CopilotModelLimits {\n\t/** `capabilities.limits.max_prompt_tokens` — maximum prompt/input budget (the hard input cap). */\n\tmaxPromptTokens?: number;\n\t/** `capabilities.limits.max_context_window_tokens` — total context capacity (the displayed long tier). */\n\tmaxContextWindowTokens?: number;\n\t/** `capabilities.limits.max_output_tokens` — output reserve; derives the input cap when `max_prompt_tokens` is absent. */\n\tmaxOutputTokens?: number;\n\t/** `billing.token_prices.default.context_max` — default-tier prompt threshold. */\n\tdefaultContextMax?: number;\n\t/** `billing.token_prices.long_context.context_max` — long-context prompt threshold. */\n\tlongContextMax?: number;\n}\n\n/**\n * Resolve a model's input-token context window(s) from its CAPI limits.\n *\n * `contextWindow` is the model's base input budget — the default tier's `context_max` when tiered,\n * otherwise `max_prompt_tokens ?? max_context_window_tokens ?? 128_000`. A `long_context` tier that\n * is larger than the base adds a second selectable window. Returns `undefined` when the entry\n * carries no usable limit signal at all.\n */\nexport function resolveCopilotModelContext(limits: CopilotModelLimits): CopilotModelContext | undefined {\n\tconst hasSignal =\n\t\tlimits.maxPromptTokens !== undefined ||\n\t\tlimits.maxContextWindowTokens !== undefined ||\n\t\tlimits.defaultContextMax !== undefined ||\n\t\tlimits.longContextMax !== undefined;\n\tif (!hasSignal) return undefined;\n\n\tconst maxInput = limits.maxPromptTokens ?? limits.maxContextWindowTokens ?? COPILOT_CONTEXT_WINDOW_FALLBACK;\n\tconst base = limits.defaultContextMax ?? maxInput;\n\tif (limits.longContextMax !== undefined && limits.longContextMax > base) {\n\t\t// Display the model's full context window as the long tier (matching openai/* and anthropic/*)\n\t\t// when CAPI advertises it; otherwise fall back to the long-context prompt threshold for\n\t\t// older/sparse payloads.\n\t\tconst longWindow = limits.maxContextWindowTokens ?? limits.longContextMax;\n\t\t// The hard prompt/input cap GitHub enforces server-side: prefer max_prompt_tokens, else derive\n\t\t// it from total − output reserve, else fall back to the long-context prompt threshold.\n\t\tconst derivedInputCap =\n\t\t\tlimits.maxContextWindowTokens !== undefined && limits.maxOutputTokens !== undefined\n\t\t\t\t? limits.maxContextWindowTokens - limits.maxOutputTokens\n\t\t\t\t: undefined;\n\t\tconst inputCap =\n\t\t\tlimits.maxPromptTokens ??\n\t\t\t(derivedInputCap !== undefined && derivedInputCap > 0 ? derivedInputCap : undefined) ??\n\t\t\tlimits.longContextMax;\n\t\t// Only carry the cap when the displayed long window actually exceeds it (the branded-total\n\t\t// case); when they coincide there is no gap and the input budget is just the window.\n\t\treturn longWindow > inputCap\n\t\t\t? { contextWindow: base, contextWindowOptions: [base, longWindow], maxInputTokens: inputCap }\n\t\t\t: { contextWindow: base, contextWindowOptions: [base, longWindow] };\n\t}\n\treturn { contextWindow: base };\n}\n\n/**\n * Parse a raw CAPI `/models` response body into an input-token context-window catalog.\n */\nexport function parseCopilotModelCatalog(body: unknown): CopilotModelCatalog {\n\tconst catalog = new Map<string, CopilotModelContext>();\n\tconst data = asRecord(body)?.data;\n\tif (!Array.isArray(data)) return catalog;\n\n\tfor (const entry of data) {\n\t\tconst record = asRecord(entry);\n\t\tif (!record) continue;\n\t\tconst id = record.id;\n\t\tif (typeof id !== \"string\" || id.length === 0) continue;\n\n\t\tconst limits = asRecord(asRecord(record.capabilities)?.limits);\n\t\tconst prices = asRecord(asRecord(record.billing)?.token_prices);\n\t\tconst context = resolveCopilotModelContext({\n\t\t\tmaxPromptTokens: toPositiveInt(limits?.max_prompt_tokens),\n\t\t\tmaxContextWindowTokens: toPositiveInt(limits?.max_context_window_tokens),\n\t\t\tmaxOutputTokens: toPositiveInt(limits?.max_output_tokens),\n\t\t\tdefaultContextMax: toPositiveInt(asRecord(prices?.default)?.context_max),\n\t\t\tlongContextMax: toPositiveInt(asRecord(prices?.long_context)?.context_max),\n\t\t});\n\t\tif (context) catalog.set(id, context);\n\t}\n\n\treturn catalog;\n}\n\nexport interface FetchCopilotModelCatalogOptions {\n\t/** Valid Copilot CAPI bearer token (e.g. from `modelRegistry.getApiKeyForProvider`). */\n\ttoken: string;\n\t/** Override the resolved base URL; defaults to one derived from the token. */\n\tbaseUrl?: string;\n\t/** Enterprise domain, used for base-URL resolution when the token lacks a `proxy-ep`. */\n\tenterpriseDomain?: string;\n\t/** Extra/override request headers. */\n\theaders?: Record<string, string>;\n\t/** Injectable `fetch` for testing. */\n\tfetchImpl?: typeof fetch;\n\t/** Abort signal. */\n\tsignal?: AbortSignal;\n}\n\n/** Fetch and parse the live Copilot model catalog from CAPI `GET {baseUrl}/models`. */\nexport async function fetchCopilotModelCatalog(options: FetchCopilotModelCatalogOptions): Promise<CopilotModelCatalog> {\n\tconst fetchImpl = options.fetchImpl ?? fetch;\n\tconst baseUrl = options.baseUrl ?? copilotApiBaseUrlFromToken(options.token, options.enterpriseDomain);\n\tconst response = await fetchImpl(`${trimTrailingSlash(baseUrl)}/models`, {\n\t\tmethod: \"GET\",\n\t\theaders: {\n\t\t\tAccept: \"application/json\",\n\t\t\tAuthorization: `Bearer ${options.token}`,\n\t\t\t...COPILOT_CATALOG_HEADERS,\n\t\t\t...options.headers,\n\t\t},\n\t\t...(options.signal ? { signal: options.signal } : {}),\n\t});\n\tif (!response.ok) {\n\t\tthrow new Error(`GitHub Copilot /models request failed: ${response.status} ${response.statusText}`);\n\t}\n\treturn parseCopilotModelCatalog(await response.json());\n}\n\n// ----------------------------------------------------------------------------\n// Active in-memory catalog (consulted by the model registry).\n//\n// Empty by default, so with no Copilot auth / no successful fetch the registry leaves Copilot\n// model context windows untouched and the picker never appears.\n// ----------------------------------------------------------------------------\n\nlet activeCatalog: CopilotModelCatalog = new Map();\n\n/** Replace the active catalog the registry derives context windows from. */\nexport function setActiveCopilotModelCatalog(catalog: CopilotModelCatalog): void {\n\tactiveCatalog = catalog;\n}\n\n/** The active catalog (empty until a successful auth-gated fetch/cache load). */\nexport function getActiveCopilotModelCatalog(): CopilotModelCatalog {\n\treturn activeCatalog;\n}\n\n/** Reset the active catalog (primarily for tests). */\nexport function clearActiveCopilotModelCatalog(): void {\n\tactiveCatalog = new Map();\n}\n\n// ----------------------------------------------------------------------------\n// Disk cache.\n// ----------------------------------------------------------------------------\n\ninterface CopilotCatalogCacheFile {\n\tversion: typeof COPILOT_CATALOG_CACHE_VERSION;\n\t/** CAPI host the catalog was fetched from; cache misses on host change (e.g. enterprise switch). */\n\thost: string;\n\t/** Epoch ms the catalog was fetched. */\n\tfetchedAt: number;\n\tmodels: Record<string, CopilotModelContext>;\n}\n\nfunction hostFromBaseUrl(baseUrl: string): string {\n\ttry {\n\t\treturn new URL(baseUrl).host;\n\t} catch {\n\t\treturn baseUrl;\n\t}\n}\n\nexport interface ReadCopilotCatalogCacheOptions {\n\t/** Expected CAPI host; a cached file from a different host is ignored. */\n\thost: string;\n\t/** Current epoch ms (injectable for tests). */\n\tnow?: number;\n\t/** Freshness window; defaults to {@link COPILOT_CATALOG_CACHE_TTL_MS}. */\n\tttlMs?: number;\n}\n\nfunction sanitizeCachedContext(value: unknown): CopilotModelContext | undefined {\n\tconst record = asRecord(value);\n\tconst contextWindow = toPositiveInt(record?.contextWindow);\n\tif (contextWindow === undefined) return undefined;\n\tconst maxInputTokens = toPositiveInt(record?.maxInputTokens);\n\tconst rawOptions = record?.contextWindowOptions;\n\tif (Array.isArray(rawOptions)) {\n\t\tconst options = rawOptions.map(toPositiveInt).filter((n): n is number => n !== undefined);\n\t\tif (options.length > 1) {\n\t\t\treturn maxInputTokens !== undefined\n\t\t\t\t? { contextWindow, contextWindowOptions: options, maxInputTokens }\n\t\t\t\t: { contextWindow, contextWindowOptions: options };\n\t\t}\n\t}\n\treturn maxInputTokens !== undefined ? { contextWindow, maxInputTokens } : { contextWindow };\n}\n\n/** Read a fresh, host-matching catalog from the cache file, or `undefined` if missing/stale/invalid. */\nexport function readCopilotCatalogCache(\n\tpath: string,\n\toptions: ReadCopilotCatalogCacheOptions,\n): CopilotModelCatalog | undefined {\n\tlet parsed: CopilotCatalogCacheFile;\n\ttry {\n\t\tif (!existsSync(path)) return undefined;\n\t\tparsed = JSON.parse(readFileSync(path, \"utf8\")) as CopilotCatalogCacheFile;\n\t} catch {\n\t\treturn undefined;\n\t}\n\tif (!parsed || parsed.version !== COPILOT_CATALOG_CACHE_VERSION) return undefined;\n\tif (parsed.host !== options.host) return undefined;\n\tconst now = options.now ?? Date.now();\n\tconst ttlMs = options.ttlMs ?? COPILOT_CATALOG_CACHE_TTL_MS;\n\tif (typeof parsed.fetchedAt !== \"number\" || now - parsed.fetchedAt >= ttlMs) return undefined;\n\tconst models = asRecord(parsed.models);\n\tif (!models) return undefined;\n\n\tconst catalog = new Map<string, CopilotModelContext>();\n\tfor (const [id, value] of Object.entries(models)) {\n\t\tconst context = sanitizeCachedContext(value);\n\t\tif (context) catalog.set(id, context);\n\t}\n\treturn catalog;\n}\n\n/** Write the catalog to the cache file (creating parent dirs). Best-effort; never throws. */\nexport function writeCopilotCatalogCache(\n\tpath: string,\n\tbaseUrl: string,\n\tcatalog: CopilotModelCatalog,\n\tnow?: number,\n): void {\n\tconst payload: CopilotCatalogCacheFile = {\n\t\tversion: COPILOT_CATALOG_CACHE_VERSION,\n\t\thost: hostFromBaseUrl(baseUrl),\n\t\tfetchedAt: now ?? Date.now(),\n\t\tmodels: Object.fromEntries(catalog),\n\t};\n\ttry {\n\t\tmkdirSync(dirname(path), { recursive: true });\n\t\twriteFileSync(path, JSON.stringify(payload), \"utf8\");\n\t} catch {\n\t\t// best-effort cache; ignore write failures\n\t}\n}\n\n/** Host component of a base URL, for matching {@link readCopilotCatalogCache} `host`. */\nexport function copilotCatalogCacheHost(baseUrl: string): string {\n\treturn hostFromBaseUrl(baseUrl);\n}\n\n/** Standard on-disk cache path for the Copilot model catalog under an agent directory. */\nexport function copilotCatalogCachePath(agentDir: string): string {\n\treturn join(agentDir, \"cache\", \"copilot-models.json\");\n}\n\n/**\n * Seed the active catalog synchronously from the on-disk cache, gated on a Copilot access token.\n *\n * Called at model-registry construction so a returning user's previously selected long-context\n * window is recognized before startup validation runs — otherwise the persisted choice would warn\n * (\"context window 936k is not supported…\") and reset until the async refresh completes. The cache\n * TTL is intentionally ignored here: stale-but-present windows are still valid for selection, and\n * the async loader independently refetches on its own freshness window. Returns true when a catalog\n * was applied. No-op (returns false) without a token or a host-matching cached catalog.\n */\nexport function seedActiveCopilotModelCatalogFromCache(\n\taccessToken: string | undefined,\n\tcachePath: string,\n\tnow?: number,\n): boolean {\n\tif (typeof accessToken !== \"string\" || accessToken.length === 0) return false;\n\tconst host = copilotCatalogCacheHost(copilotApiBaseUrlFromToken(accessToken));\n\tconst cached = readCopilotCatalogCache(cachePath, { host, now, ttlMs: Number.POSITIVE_INFINITY });\n\tif (!cached) return false;\n\tsetActiveCopilotModelCatalog(cached);\n\treturn true;\n}\n"]}
@@ -41,8 +41,57 @@ export const COPILOT_CATALOG_HEADERS = {
41
41
  "Copilot-Integration-Id": "vscode-chat",
42
42
  "X-GitHub-Api-Version": COPILOT_CATALOG_API_VERSION,
43
43
  };
44
- /** Default (non-enterprise) Copilot CAPI base URL when the token has no resolvable `proxy-ep`. */
44
+ /** Default Copilot CAPI base URL for OAuth tokens when no route can be resolved. */
45
45
  export const DEFAULT_COPILOT_API_BASE_URL = "https://api.individual.githubcopilot.com";
46
+ /** GitHub Copilot public routing hub for PAT-based `COPILOT_GITHUB_TOKEN` auth. */
47
+ export const COPILOT_PUBLIC_API_BASE_URL = "https://api.githubcopilot.com";
48
+ /** Enterprise Copilot CAPI host for GHES/non-github.com server URLs. */
49
+ export const COPILOT_ENTERPRISE_API_BASE_URL = "https://api.enterprise.githubcopilot.com";
50
+ export const COPILOT_GITHUB_TOKEN_ENV = "COPILOT_GITHUB_TOKEN";
51
+ export const COPILOT_API_TARGET_ENV = "COPILOT_API_TARGET";
52
+ export const GITHUB_COPILOT_BASE_URL_ENV = "GITHUB_COPILOT_BASE_URL";
53
+ export const GITHUB_SERVER_URL_ENV = "GITHUB_SERVER_URL";
54
+ function normalizeCopilotApiBaseUrl(value) {
55
+ const trimmed = value?.trim();
56
+ if (!trimmed)
57
+ return undefined;
58
+ const withScheme = /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
59
+ try {
60
+ new URL(withScheme);
61
+ }
62
+ catch {
63
+ return undefined;
64
+ }
65
+ return withScheme.replace(/\/+$/, "");
66
+ }
67
+ function explicitCopilotApiBaseUrlFromEnvironment(env) {
68
+ return normalizeCopilotApiBaseUrl(env[COPILOT_API_TARGET_ENV]) ?? normalizeCopilotApiBaseUrl(env[GITHUB_COPILOT_BASE_URL_ENV]);
69
+ }
70
+ function copilotApiBaseUrlFromServerUrl(serverUrl) {
71
+ const trimmed = serverUrl?.trim();
72
+ if (!trimmed)
73
+ return undefined;
74
+ try {
75
+ const parsed = new URL(trimmed.includes("://") ? trimmed : `https://${trimmed}`);
76
+ const host = parsed.hostname.toLowerCase();
77
+ if (!host || host === "github.com")
78
+ return undefined;
79
+ if (host.endsWith(".ghe.com"))
80
+ return `https://copilot-api.${host}`;
81
+ return COPILOT_ENTERPRISE_API_BASE_URL;
82
+ }
83
+ catch {
84
+ return undefined;
85
+ }
86
+ }
87
+ function isCopilotEnvironmentToken(token, env) {
88
+ const environmentToken = copilotTokenFromEnvironment(env);
89
+ return environmentToken !== undefined && token === environmentToken;
90
+ }
91
+ export function copilotTokenFromEnvironment(env = process.env) {
92
+ const token = env[COPILOT_GITHUB_TOKEN_ENV]?.trim();
93
+ return token ? token : undefined;
94
+ }
46
95
  /** Disk-cache freshness window, matching the Copilot CLI's list-models cache TTL. */
47
96
  export const COPILOT_CATALOG_CACHE_TTL_MS = 30 * 60 * 1000;
48
97
  /** Current on-disk cache schema version. */
@@ -51,11 +100,16 @@ export const COPILOT_CATALOG_CACHE_VERSION = 3;
51
100
  * Resolve the Copilot CAPI base URL.
52
101
  *
53
102
  * Copilot access tokens embed a `proxy-ep=proxy.<host>` segment; the API host is the same host with
54
- * `proxy.` swapped for `api.`. Falls back to the enterprise host or the individual default. (pi-ai
55
- * exposes an equivalent helper, but its published `dist` mangles the export name, so the small,
56
- * stable parsing logic is reimplemented here.)
103
+ * `proxy.` swapped for `api.`. Env-token routing resolves explicit `COPILOT_API_TARGET` /
104
+ * `GITHUB_COPILOT_BASE_URL` overrides, then `GITHUB_SERVER_URL` (`*.ghe.com` ->
105
+ * `copilot-api.<tenant>.ghe.com`, other non-github.com -> `api.enterprise.githubcopilot.com`),
106
+ * then the public Copilot routing hub `api.githubcopilot.com` for `COPILOT_GITHUB_TOKEN`. Stored
107
+ * OAuth credentials still fall back to the generated individual host when no token route is known.
57
108
  */
58
- export function copilotApiBaseUrlFromToken(token, enterpriseDomain) {
109
+ export function copilotApiBaseUrlFromToken(token, enterpriseDomain, env = process.env) {
110
+ const explicitOverride = explicitCopilotApiBaseUrlFromEnvironment(env);
111
+ if (explicitOverride)
112
+ return explicitOverride;
59
113
  if (token) {
60
114
  const match = token.match(/proxy-ep=([^;]+)/);
61
115
  if (match) {
@@ -64,6 +118,11 @@ export function copilotApiBaseUrlFromToken(token, enterpriseDomain) {
64
118
  }
65
119
  if (enterpriseDomain)
66
120
  return `https://copilot-api.${enterpriseDomain}`;
121
+ const serverUrlOverride = copilotApiBaseUrlFromServerUrl(env[GITHUB_SERVER_URL_ENV]);
122
+ if (serverUrlOverride)
123
+ return serverUrlOverride;
124
+ if (isCopilotEnvironmentToken(token, env))
125
+ return COPILOT_PUBLIC_API_BASE_URL;
67
126
  return DEFAULT_COPILOT_API_BASE_URL;
68
127
  }
69
128
  function trimTrailingSlash(url) {
@@ -1 +1 @@
1
- {"version":3,"file":"copilot-model-catalog.js","sourceRoot":"","sources":["../../src/core/copilot-model-catalog.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * GitHub Copilot model catalog (CAPI) — dynamic prompt-token budgets.\n *\n * GitHub's Copilot API (CAPI) exposes distinct model limits via `GET {baseUrl}/models`:\n *\n * - `capabilities.limits.max_context_window_tokens` is the model's total context capacity\n * (prompt + completion reserve).\n * - `capabilities.limits.max_prompt_tokens` is the maximum prompt/input budget Atomic can safely\n * fill before the provider must reserve output tokens.\n * - `billing.token_prices.<tier>.context_max` is a prompt-token billing/selection threshold. The\n * `default` tier is the short prompt budget (e.g. gpt-5.5 272k, Claude 200k); a\n * `long_context` tier adds a selectable larger prompt budget (e.g. gpt-5.5 922k, Claude 936k).\n *\n * Atomic shows the model's full context window for the selectable long tier (the\n * `max_context_window_tokens` total, e.g. 1_000_000/1_050_000), matching how the native `openai/*`\n * and `anthropic/*` providers advertise these models. Because GitHub enforces a lower server-side\n * prompt cap (`max_prompt_tokens`, e.g. 936k/922k) below that total, the prompt cap is retained as\n * an internal effective input budget (`CopilotModelContext.maxInputTokens`) that drives compaction\n * thresholds and the overflow-recovery guard, so the branded total can be displayed without\n * overrunning the server limit. The default (short) tier stays at the `default` billing tier's\n * prompt budget.\n *\n * This data is intentionally NOT baked into a static map: GitHub adds/removes models and retiers\n * windows over time (e.g. a model that disappears from the catalog), so a hardcoded snapshot goes\n * stale. Instead the catalog is fetched live (gated on the user actually having the GitHub Copilot\n * provider) and cached on disk for a short TTL, exactly like the Copilot CLI.\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\n\n/** Resolved input-token context window(s) for a single Copilot model. */\nexport interface CopilotModelContext {\n\t/**\n\t * Base/displayed context window — shown in the footer. The default tier's `context_max`, or the\n\t * model-level `max_prompt_tokens` fallback otherwise.\n\t */\n\tcontextWindow: number;\n\t/**\n\t * Selectable windows (`[default, long]`) when the model exposes a `long_context` tier larger than\n\t * its default; absent for single-window models. The long entry is the model's full\n\t * `max_context_window_tokens` (total capacity) when advertised, matching `openai/*` and\n\t * `anthropic/*`.\n\t */\n\tcontextWindowOptions?: readonly number[];\n\t/**\n\t * Hard prompt/input cap (`max_prompt_tokens`) when it sits below the displayed long window. Used\n\t * as the effective input budget for compaction thresholds and overflow recovery so the branded\n\t * total can be shown without overrunning GitHub's server-side prompt limit. Absent when the\n\t * displayed window already equals the input cap.\n\t */\n\tmaxInputTokens?: number;\n}\n\n/** Map of model id → resolved input-token context window(s). */\nexport type CopilotModelCatalog = ReadonlyMap<string, CopilotModelContext>;\n\n/** Safety fallback when a model reports neither `max_prompt_tokens` nor `max_context_window_tokens`. */\nexport const COPILOT_CONTEXT_WINDOW_FALLBACK = 128_000;\n\nexport const COPILOT_CATALOG_API_VERSION = \"2026-06-01\";\n\n/**\n * Headers GitHub's CAPI expects for catalog reads. Mirrors the editor headers pi-ai already sends\n * for Copilot token refresh and model-policy calls, plus the dated API version.\n */\nexport const COPILOT_CATALOG_HEADERS: Readonly<Record<string, string>> = {\n\t\"User-Agent\": \"GitHubCopilotChat/0.35.0\",\n\t\"Editor-Version\": \"vscode/1.107.0\",\n\t\"Editor-Plugin-Version\": \"copilot-chat/0.35.0\",\n\t\"Copilot-Integration-Id\": \"vscode-chat\",\n\t\"X-GitHub-Api-Version\": COPILOT_CATALOG_API_VERSION,\n};\n\n/** Default (non-enterprise) Copilot CAPI base URL when the token has no resolvable `proxy-ep`. */\nexport const DEFAULT_COPILOT_API_BASE_URL = \"https://api.individual.githubcopilot.com\";\n\n/** Disk-cache freshness window, matching the Copilot CLI's list-models cache TTL. */\nexport const COPILOT_CATALOG_CACHE_TTL_MS = 30 * 60 * 1000;\n\n/** Current on-disk cache schema version. */\nexport const COPILOT_CATALOG_CACHE_VERSION = 3 as const;\n\n/**\n * Resolve the Copilot CAPI base URL.\n *\n * Copilot access tokens embed a `proxy-ep=proxy.<host>` segment; the API host is the same host with\n * `proxy.` swapped for `api.`. Falls back to the enterprise host or the individual default. (pi-ai\n * exposes an equivalent helper, but its published `dist` mangles the export name, so the small,\n * stable parsing logic is reimplemented here.)\n */\nexport function copilotApiBaseUrlFromToken(token: string | undefined, enterpriseDomain?: string): string {\n\tif (token) {\n\t\tconst match = token.match(/proxy-ep=([^;]+)/);\n\t\tif (match) {\n\t\t\treturn `https://${match[1].replace(/^proxy\\./, \"api.\")}`;\n\t\t}\n\t}\n\tif (enterpriseDomain) return `https://copilot-api.${enterpriseDomain}`;\n\treturn DEFAULT_COPILOT_API_BASE_URL;\n}\n\nfunction trimTrailingSlash(url: string): string {\n\treturn url.replace(/\\/+$/, \"\");\n}\n\nfunction asRecord(value: unknown): Record<string, unknown> | undefined {\n\treturn value && typeof value === \"object\" ? (value as Record<string, unknown>) : undefined;\n}\n\nfunction toPositiveInt(value: unknown): number | undefined {\n\treturn typeof value === \"number\" && Number.isInteger(value) && value > 0 ? value : undefined;\n}\n\n/** Raw token limits parsed from a CAPI model entry. */\nexport interface CopilotModelLimits {\n\t/** `capabilities.limits.max_prompt_tokens` — maximum prompt/input budget (the hard input cap). */\n\tmaxPromptTokens?: number;\n\t/** `capabilities.limits.max_context_window_tokens` — total context capacity (the displayed long tier). */\n\tmaxContextWindowTokens?: number;\n\t/** `capabilities.limits.max_output_tokens` — output reserve; derives the input cap when `max_prompt_tokens` is absent. */\n\tmaxOutputTokens?: number;\n\t/** `billing.token_prices.default.context_max` — default-tier prompt threshold. */\n\tdefaultContextMax?: number;\n\t/** `billing.token_prices.long_context.context_max` — long-context prompt threshold. */\n\tlongContextMax?: number;\n}\n\n/**\n * Resolve a model's input-token context window(s) from its CAPI limits.\n *\n * `contextWindow` is the model's base input budget — the default tier's `context_max` when tiered,\n * otherwise `max_prompt_tokens ?? max_context_window_tokens ?? 128_000`. A `long_context` tier that\n * is larger than the base adds a second selectable window. Returns `undefined` when the entry\n * carries no usable limit signal at all.\n */\nexport function resolveCopilotModelContext(limits: CopilotModelLimits): CopilotModelContext | undefined {\n\tconst hasSignal =\n\t\tlimits.maxPromptTokens !== undefined ||\n\t\tlimits.maxContextWindowTokens !== undefined ||\n\t\tlimits.defaultContextMax !== undefined ||\n\t\tlimits.longContextMax !== undefined;\n\tif (!hasSignal) return undefined;\n\n\tconst maxInput = limits.maxPromptTokens ?? limits.maxContextWindowTokens ?? COPILOT_CONTEXT_WINDOW_FALLBACK;\n\tconst base = limits.defaultContextMax ?? maxInput;\n\tif (limits.longContextMax !== undefined && limits.longContextMax > base) {\n\t\t// Display the model's full context window as the long tier (matching openai/* and anthropic/*)\n\t\t// when CAPI advertises it; otherwise fall back to the long-context prompt threshold for\n\t\t// older/sparse payloads.\n\t\tconst longWindow = limits.maxContextWindowTokens ?? limits.longContextMax;\n\t\t// The hard prompt/input cap GitHub enforces server-side: prefer max_prompt_tokens, else derive\n\t\t// it from total − output reserve, else fall back to the long-context prompt threshold.\n\t\tconst derivedInputCap =\n\t\t\tlimits.maxContextWindowTokens !== undefined && limits.maxOutputTokens !== undefined\n\t\t\t\t? limits.maxContextWindowTokens - limits.maxOutputTokens\n\t\t\t\t: undefined;\n\t\tconst inputCap =\n\t\t\tlimits.maxPromptTokens ??\n\t\t\t(derivedInputCap !== undefined && derivedInputCap > 0 ? derivedInputCap : undefined) ??\n\t\t\tlimits.longContextMax;\n\t\t// Only carry the cap when the displayed long window actually exceeds it (the branded-total\n\t\t// case); when they coincide there is no gap and the input budget is just the window.\n\t\treturn longWindow > inputCap\n\t\t\t? { contextWindow: base, contextWindowOptions: [base, longWindow], maxInputTokens: inputCap }\n\t\t\t: { contextWindow: base, contextWindowOptions: [base, longWindow] };\n\t}\n\treturn { contextWindow: base };\n}\n\n/**\n * Parse a raw CAPI `/models` response body into an input-token context-window catalog.\n */\nexport function parseCopilotModelCatalog(body: unknown): CopilotModelCatalog {\n\tconst catalog = new Map<string, CopilotModelContext>();\n\tconst data = asRecord(body)?.data;\n\tif (!Array.isArray(data)) return catalog;\n\n\tfor (const entry of data) {\n\t\tconst record = asRecord(entry);\n\t\tif (!record) continue;\n\t\tconst id = record.id;\n\t\tif (typeof id !== \"string\" || id.length === 0) continue;\n\n\t\tconst limits = asRecord(asRecord(record.capabilities)?.limits);\n\t\tconst prices = asRecord(asRecord(record.billing)?.token_prices);\n\t\tconst context = resolveCopilotModelContext({\n\t\t\tmaxPromptTokens: toPositiveInt(limits?.max_prompt_tokens),\n\t\t\tmaxContextWindowTokens: toPositiveInt(limits?.max_context_window_tokens),\n\t\t\tmaxOutputTokens: toPositiveInt(limits?.max_output_tokens),\n\t\t\tdefaultContextMax: toPositiveInt(asRecord(prices?.default)?.context_max),\n\t\t\tlongContextMax: toPositiveInt(asRecord(prices?.long_context)?.context_max),\n\t\t});\n\t\tif (context) catalog.set(id, context);\n\t}\n\n\treturn catalog;\n}\n\nexport interface FetchCopilotModelCatalogOptions {\n\t/** Valid Copilot CAPI bearer token (e.g. from `modelRegistry.getApiKeyForProvider`). */\n\ttoken: string;\n\t/** Override the resolved base URL; defaults to one derived from the token. */\n\tbaseUrl?: string;\n\t/** Enterprise domain, used for base-URL resolution when the token lacks a `proxy-ep`. */\n\tenterpriseDomain?: string;\n\t/** Extra/override request headers. */\n\theaders?: Record<string, string>;\n\t/** Injectable `fetch` for testing. */\n\tfetchImpl?: typeof fetch;\n\t/** Abort signal. */\n\tsignal?: AbortSignal;\n}\n\n/** Fetch and parse the live Copilot model catalog from CAPI `GET {baseUrl}/models`. */\nexport async function fetchCopilotModelCatalog(options: FetchCopilotModelCatalogOptions): Promise<CopilotModelCatalog> {\n\tconst fetchImpl = options.fetchImpl ?? fetch;\n\tconst baseUrl = options.baseUrl ?? copilotApiBaseUrlFromToken(options.token, options.enterpriseDomain);\n\tconst response = await fetchImpl(`${trimTrailingSlash(baseUrl)}/models`, {\n\t\tmethod: \"GET\",\n\t\theaders: {\n\t\t\tAccept: \"application/json\",\n\t\t\tAuthorization: `Bearer ${options.token}`,\n\t\t\t...COPILOT_CATALOG_HEADERS,\n\t\t\t...options.headers,\n\t\t},\n\t\t...(options.signal ? { signal: options.signal } : {}),\n\t});\n\tif (!response.ok) {\n\t\tthrow new Error(`GitHub Copilot /models request failed: ${response.status} ${response.statusText}`);\n\t}\n\treturn parseCopilotModelCatalog(await response.json());\n}\n\n// ----------------------------------------------------------------------------\n// Active in-memory catalog (consulted by the model registry).\n//\n// Empty by default, so with no Copilot auth / no successful fetch the registry leaves Copilot\n// model context windows untouched and the picker never appears.\n// ----------------------------------------------------------------------------\n\nlet activeCatalog: CopilotModelCatalog = new Map();\n\n/** Replace the active catalog the registry derives context windows from. */\nexport function setActiveCopilotModelCatalog(catalog: CopilotModelCatalog): void {\n\tactiveCatalog = catalog;\n}\n\n/** The active catalog (empty until a successful auth-gated fetch/cache load). */\nexport function getActiveCopilotModelCatalog(): CopilotModelCatalog {\n\treturn activeCatalog;\n}\n\n/** Reset the active catalog (primarily for tests). */\nexport function clearActiveCopilotModelCatalog(): void {\n\tactiveCatalog = new Map();\n}\n\n// ----------------------------------------------------------------------------\n// Disk cache.\n// ----------------------------------------------------------------------------\n\ninterface CopilotCatalogCacheFile {\n\tversion: typeof COPILOT_CATALOG_CACHE_VERSION;\n\t/** CAPI host the catalog was fetched from; cache misses on host change (e.g. enterprise switch). */\n\thost: string;\n\t/** Epoch ms the catalog was fetched. */\n\tfetchedAt: number;\n\tmodels: Record<string, CopilotModelContext>;\n}\n\nfunction hostFromBaseUrl(baseUrl: string): string {\n\ttry {\n\t\treturn new URL(baseUrl).host;\n\t} catch {\n\t\treturn baseUrl;\n\t}\n}\n\nexport interface ReadCopilotCatalogCacheOptions {\n\t/** Expected CAPI host; a cached file from a different host is ignored. */\n\thost: string;\n\t/** Current epoch ms (injectable for tests). */\n\tnow?: number;\n\t/** Freshness window; defaults to {@link COPILOT_CATALOG_CACHE_TTL_MS}. */\n\tttlMs?: number;\n}\n\nfunction sanitizeCachedContext(value: unknown): CopilotModelContext | undefined {\n\tconst record = asRecord(value);\n\tconst contextWindow = toPositiveInt(record?.contextWindow);\n\tif (contextWindow === undefined) return undefined;\n\tconst maxInputTokens = toPositiveInt(record?.maxInputTokens);\n\tconst rawOptions = record?.contextWindowOptions;\n\tif (Array.isArray(rawOptions)) {\n\t\tconst options = rawOptions.map(toPositiveInt).filter((n): n is number => n !== undefined);\n\t\tif (options.length > 1) {\n\t\t\treturn maxInputTokens !== undefined\n\t\t\t\t? { contextWindow, contextWindowOptions: options, maxInputTokens }\n\t\t\t\t: { contextWindow, contextWindowOptions: options };\n\t\t}\n\t}\n\treturn maxInputTokens !== undefined ? { contextWindow, maxInputTokens } : { contextWindow };\n}\n\n/** Read a fresh, host-matching catalog from the cache file, or `undefined` if missing/stale/invalid. */\nexport function readCopilotCatalogCache(\n\tpath: string,\n\toptions: ReadCopilotCatalogCacheOptions,\n): CopilotModelCatalog | undefined {\n\tlet parsed: CopilotCatalogCacheFile;\n\ttry {\n\t\tif (!existsSync(path)) return undefined;\n\t\tparsed = JSON.parse(readFileSync(path, \"utf8\")) as CopilotCatalogCacheFile;\n\t} catch {\n\t\treturn undefined;\n\t}\n\tif (!parsed || parsed.version !== COPILOT_CATALOG_CACHE_VERSION) return undefined;\n\tif (parsed.host !== options.host) return undefined;\n\tconst now = options.now ?? Date.now();\n\tconst ttlMs = options.ttlMs ?? COPILOT_CATALOG_CACHE_TTL_MS;\n\tif (typeof parsed.fetchedAt !== \"number\" || now - parsed.fetchedAt >= ttlMs) return undefined;\n\tconst models = asRecord(parsed.models);\n\tif (!models) return undefined;\n\n\tconst catalog = new Map<string, CopilotModelContext>();\n\tfor (const [id, value] of Object.entries(models)) {\n\t\tconst context = sanitizeCachedContext(value);\n\t\tif (context) catalog.set(id, context);\n\t}\n\treturn catalog;\n}\n\n/** Write the catalog to the cache file (creating parent dirs). Best-effort; never throws. */\nexport function writeCopilotCatalogCache(\n\tpath: string,\n\tbaseUrl: string,\n\tcatalog: CopilotModelCatalog,\n\tnow?: number,\n): void {\n\tconst payload: CopilotCatalogCacheFile = {\n\t\tversion: COPILOT_CATALOG_CACHE_VERSION,\n\t\thost: hostFromBaseUrl(baseUrl),\n\t\tfetchedAt: now ?? Date.now(),\n\t\tmodels: Object.fromEntries(catalog),\n\t};\n\ttry {\n\t\tmkdirSync(dirname(path), { recursive: true });\n\t\twriteFileSync(path, JSON.stringify(payload), \"utf8\");\n\t} catch {\n\t\t// best-effort cache; ignore write failures\n\t}\n}\n\n/** Host component of a base URL, for matching {@link readCopilotCatalogCache} `host`. */\nexport function copilotCatalogCacheHost(baseUrl: string): string {\n\treturn hostFromBaseUrl(baseUrl);\n}\n\n/** Standard on-disk cache path for the Copilot model catalog under an agent directory. */\nexport function copilotCatalogCachePath(agentDir: string): string {\n\treturn join(agentDir, \"cache\", \"copilot-models.json\");\n}\n\n/**\n * Seed the active catalog synchronously from the on-disk cache, gated on a Copilot access token.\n *\n * Called at model-registry construction so a returning user's previously selected long-context\n * window is recognized before startup validation runs — otherwise the persisted choice would warn\n * (\"context window 936k is not supported…\") and reset until the async refresh completes. The cache\n * TTL is intentionally ignored here: stale-but-present windows are still valid for selection, and\n * the async loader independently refetches on its own freshness window. Returns true when a catalog\n * was applied. No-op (returns false) without a token or a host-matching cached catalog.\n */\nexport function seedActiveCopilotModelCatalogFromCache(\n\taccessToken: string | undefined,\n\tcachePath: string,\n\tnow?: number,\n): boolean {\n\tif (typeof accessToken !== \"string\" || accessToken.length === 0) return false;\n\tconst host = copilotCatalogCacheHost(copilotApiBaseUrlFromToken(accessToken));\n\tconst cached = readCopilotCatalogCache(cachePath, { host, now, ttlMs: Number.POSITIVE_INFINITY });\n\tif (!cached) return false;\n\tsetActiveCopilotModelCatalog(cached);\n\treturn true;\n}\n"]}
1
+ {"version":3,"file":"copilot-model-catalog.js","sourceRoot":"","sources":["../../src/core/copilot-model-catalog.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * GitHub Copilot model catalog (CAPI) — dynamic prompt-token budgets.\n *\n * GitHub's Copilot API (CAPI) exposes distinct model limits via `GET {baseUrl}/models`:\n *\n * - `capabilities.limits.max_context_window_tokens` is the model's total context capacity\n * (prompt + completion reserve).\n * - `capabilities.limits.max_prompt_tokens` is the maximum prompt/input budget Atomic can safely\n * fill before the provider must reserve output tokens.\n * - `billing.token_prices.<tier>.context_max` is a prompt-token billing/selection threshold. The\n * `default` tier is the short prompt budget (e.g. gpt-5.5 272k, Claude 200k); a\n * `long_context` tier adds a selectable larger prompt budget (e.g. gpt-5.5 922k, Claude 936k).\n *\n * Atomic shows the model's full context window for the selectable long tier (the\n * `max_context_window_tokens` total, e.g. 1_000_000/1_050_000), matching how the native `openai/*`\n * and `anthropic/*` providers advertise these models. Because GitHub enforces a lower server-side\n * prompt cap (`max_prompt_tokens`, e.g. 936k/922k) below that total, the prompt cap is retained as\n * an internal effective input budget (`CopilotModelContext.maxInputTokens`) that drives compaction\n * thresholds and the overflow-recovery guard, so the branded total can be displayed without\n * overrunning the server limit. The default (short) tier stays at the `default` billing tier's\n * prompt budget.\n *\n * This data is intentionally NOT baked into a static map: GitHub adds/removes models and retiers\n * windows over time (e.g. a model that disappears from the catalog), so a hardcoded snapshot goes\n * stale. Instead the catalog is fetched live (gated on the user actually having the GitHub Copilot\n * provider) and cached on disk for a short TTL, exactly like the Copilot CLI.\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\n\n/** Resolved input-token context window(s) for a single Copilot model. */\nexport interface CopilotModelContext {\n\t/**\n\t * Base/displayed context window — shown in the footer. The default tier's `context_max`, or the\n\t * model-level `max_prompt_tokens` fallback otherwise.\n\t */\n\tcontextWindow: number;\n\t/**\n\t * Selectable windows (`[default, long]`) when the model exposes a `long_context` tier larger than\n\t * its default; absent for single-window models. The long entry is the model's full\n\t * `max_context_window_tokens` (total capacity) when advertised, matching `openai/*` and\n\t * `anthropic/*`.\n\t */\n\tcontextWindowOptions?: readonly number[];\n\t/**\n\t * Hard prompt/input cap (`max_prompt_tokens`) when it sits below the displayed long window. Used\n\t * as the effective input budget for compaction thresholds and overflow recovery so the branded\n\t * total can be shown without overrunning GitHub's server-side prompt limit. Absent when the\n\t * displayed window already equals the input cap.\n\t */\n\tmaxInputTokens?: number;\n}\n\n/** Map of model id → resolved input-token context window(s). */\nexport type CopilotModelCatalog = ReadonlyMap<string, CopilotModelContext>;\n\n/** Safety fallback when a model reports neither `max_prompt_tokens` nor `max_context_window_tokens`. */\nexport const COPILOT_CONTEXT_WINDOW_FALLBACK = 128_000;\n\nexport const COPILOT_CATALOG_API_VERSION = \"2026-06-01\";\n\n/**\n * Headers GitHub's CAPI expects for catalog reads. Mirrors the editor headers pi-ai already sends\n * for Copilot token refresh and model-policy calls, plus the dated API version.\n */\nexport const COPILOT_CATALOG_HEADERS: Readonly<Record<string, string>> = {\n\t\"User-Agent\": \"GitHubCopilotChat/0.35.0\",\n\t\"Editor-Version\": \"vscode/1.107.0\",\n\t\"Editor-Plugin-Version\": \"copilot-chat/0.35.0\",\n\t\"Copilot-Integration-Id\": \"vscode-chat\",\n\t\"X-GitHub-Api-Version\": COPILOT_CATALOG_API_VERSION,\n};\n\n/** Default Copilot CAPI base URL for OAuth tokens when no route can be resolved. */\nexport const DEFAULT_COPILOT_API_BASE_URL = \"https://api.individual.githubcopilot.com\";\n\n/** GitHub Copilot public routing hub for PAT-based `COPILOT_GITHUB_TOKEN` auth. */\nexport const COPILOT_PUBLIC_API_BASE_URL = \"https://api.githubcopilot.com\";\n\n/** Enterprise Copilot CAPI host for GHES/non-github.com server URLs. */\nexport const COPILOT_ENTERPRISE_API_BASE_URL = \"https://api.enterprise.githubcopilot.com\";\n\nexport const COPILOT_GITHUB_TOKEN_ENV = \"COPILOT_GITHUB_TOKEN\";\nexport const COPILOT_API_TARGET_ENV = \"COPILOT_API_TARGET\";\nexport const GITHUB_COPILOT_BASE_URL_ENV = \"GITHUB_COPILOT_BASE_URL\";\nexport const GITHUB_SERVER_URL_ENV = \"GITHUB_SERVER_URL\";\n\ntype CopilotEnvironment = Record<string, string | undefined>;\n\nfunction normalizeCopilotApiBaseUrl(value: string | undefined): string | undefined {\n\tconst trimmed = value?.trim();\n\tif (!trimmed) return undefined;\n\tconst withScheme = /^https?:\\/\\//i.test(trimmed) ? trimmed : `https://${trimmed}`;\n\ttry {\n\t\tnew URL(withScheme);\n\t} catch {\n\t\treturn undefined;\n\t}\n\treturn withScheme.replace(/\\/+$/, \"\");\n}\n\nfunction explicitCopilotApiBaseUrlFromEnvironment(env: CopilotEnvironment): string | undefined {\n\treturn normalizeCopilotApiBaseUrl(env[COPILOT_API_TARGET_ENV]) ?? normalizeCopilotApiBaseUrl(env[GITHUB_COPILOT_BASE_URL_ENV]);\n}\n\nfunction copilotApiBaseUrlFromServerUrl(serverUrl: string | undefined): string | undefined {\n\tconst trimmed = serverUrl?.trim();\n\tif (!trimmed) return undefined;\n\ttry {\n\t\tconst parsed = new URL(trimmed.includes(\"://\") ? trimmed : `https://${trimmed}`);\n\t\tconst host = parsed.hostname.toLowerCase();\n\t\tif (!host || host === \"github.com\") return undefined;\n\t\tif (host.endsWith(\".ghe.com\")) return `https://copilot-api.${host}`;\n\t\treturn COPILOT_ENTERPRISE_API_BASE_URL;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nfunction isCopilotEnvironmentToken(token: string | undefined, env: CopilotEnvironment): boolean {\n\tconst environmentToken = copilotTokenFromEnvironment(env);\n\treturn environmentToken !== undefined && token === environmentToken;\n}\n\nexport function copilotTokenFromEnvironment(env: CopilotEnvironment = process.env): string | undefined {\n\tconst token = env[COPILOT_GITHUB_TOKEN_ENV]?.trim();\n\treturn token ? token : undefined;\n}\n\n/** Disk-cache freshness window, matching the Copilot CLI's list-models cache TTL. */\nexport const COPILOT_CATALOG_CACHE_TTL_MS = 30 * 60 * 1000;\n\n/** Current on-disk cache schema version. */\nexport const COPILOT_CATALOG_CACHE_VERSION = 3 as const;\n\n/**\n * Resolve the Copilot CAPI base URL.\n *\n * Copilot access tokens embed a `proxy-ep=proxy.<host>` segment; the API host is the same host with\n * `proxy.` swapped for `api.`. Env-token routing resolves explicit `COPILOT_API_TARGET` /\n * `GITHUB_COPILOT_BASE_URL` overrides, then `GITHUB_SERVER_URL` (`*.ghe.com` ->\n * `copilot-api.<tenant>.ghe.com`, other non-github.com -> `api.enterprise.githubcopilot.com`),\n * then the public Copilot routing hub `api.githubcopilot.com` for `COPILOT_GITHUB_TOKEN`. Stored\n * OAuth credentials still fall back to the generated individual host when no token route is known.\n */\nexport function copilotApiBaseUrlFromToken(\n\ttoken: string | undefined,\n\tenterpriseDomain?: string,\n\tenv: CopilotEnvironment = process.env,\n): string {\n\tconst explicitOverride = explicitCopilotApiBaseUrlFromEnvironment(env);\n\tif (explicitOverride) return explicitOverride;\n\tif (token) {\n\t\tconst match = token.match(/proxy-ep=([^;]+)/);\n\t\tif (match) {\n\t\t\treturn `https://${match[1].replace(/^proxy\\./, \"api.\")}`;\n\t\t}\n\t}\n\tif (enterpriseDomain) return `https://copilot-api.${enterpriseDomain}`;\n\tconst serverUrlOverride = copilotApiBaseUrlFromServerUrl(env[GITHUB_SERVER_URL_ENV]);\n\tif (serverUrlOverride) return serverUrlOverride;\n\tif (isCopilotEnvironmentToken(token, env)) return COPILOT_PUBLIC_API_BASE_URL;\n\treturn DEFAULT_COPILOT_API_BASE_URL;\n}\n\nfunction trimTrailingSlash(url: string): string {\n\treturn url.replace(/\\/+$/, \"\");\n}\n\nfunction asRecord(value: unknown): Record<string, unknown> | undefined {\n\treturn value && typeof value === \"object\" ? (value as Record<string, unknown>) : undefined;\n}\n\nfunction toPositiveInt(value: unknown): number | undefined {\n\treturn typeof value === \"number\" && Number.isInteger(value) && value > 0 ? value : undefined;\n}\n\n/** Raw token limits parsed from a CAPI model entry. */\nexport interface CopilotModelLimits {\n\t/** `capabilities.limits.max_prompt_tokens` — maximum prompt/input budget (the hard input cap). */\n\tmaxPromptTokens?: number;\n\t/** `capabilities.limits.max_context_window_tokens` — total context capacity (the displayed long tier). */\n\tmaxContextWindowTokens?: number;\n\t/** `capabilities.limits.max_output_tokens` — output reserve; derives the input cap when `max_prompt_tokens` is absent. */\n\tmaxOutputTokens?: number;\n\t/** `billing.token_prices.default.context_max` — default-tier prompt threshold. */\n\tdefaultContextMax?: number;\n\t/** `billing.token_prices.long_context.context_max` — long-context prompt threshold. */\n\tlongContextMax?: number;\n}\n\n/**\n * Resolve a model's input-token context window(s) from its CAPI limits.\n *\n * `contextWindow` is the model's base input budget — the default tier's `context_max` when tiered,\n * otherwise `max_prompt_tokens ?? max_context_window_tokens ?? 128_000`. A `long_context` tier that\n * is larger than the base adds a second selectable window. Returns `undefined` when the entry\n * carries no usable limit signal at all.\n */\nexport function resolveCopilotModelContext(limits: CopilotModelLimits): CopilotModelContext | undefined {\n\tconst hasSignal =\n\t\tlimits.maxPromptTokens !== undefined ||\n\t\tlimits.maxContextWindowTokens !== undefined ||\n\t\tlimits.defaultContextMax !== undefined ||\n\t\tlimits.longContextMax !== undefined;\n\tif (!hasSignal) return undefined;\n\n\tconst maxInput = limits.maxPromptTokens ?? limits.maxContextWindowTokens ?? COPILOT_CONTEXT_WINDOW_FALLBACK;\n\tconst base = limits.defaultContextMax ?? maxInput;\n\tif (limits.longContextMax !== undefined && limits.longContextMax > base) {\n\t\t// Display the model's full context window as the long tier (matching openai/* and anthropic/*)\n\t\t// when CAPI advertises it; otherwise fall back to the long-context prompt threshold for\n\t\t// older/sparse payloads.\n\t\tconst longWindow = limits.maxContextWindowTokens ?? limits.longContextMax;\n\t\t// The hard prompt/input cap GitHub enforces server-side: prefer max_prompt_tokens, else derive\n\t\t// it from total − output reserve, else fall back to the long-context prompt threshold.\n\t\tconst derivedInputCap =\n\t\t\tlimits.maxContextWindowTokens !== undefined && limits.maxOutputTokens !== undefined\n\t\t\t\t? limits.maxContextWindowTokens - limits.maxOutputTokens\n\t\t\t\t: undefined;\n\t\tconst inputCap =\n\t\t\tlimits.maxPromptTokens ??\n\t\t\t(derivedInputCap !== undefined && derivedInputCap > 0 ? derivedInputCap : undefined) ??\n\t\t\tlimits.longContextMax;\n\t\t// Only carry the cap when the displayed long window actually exceeds it (the branded-total\n\t\t// case); when they coincide there is no gap and the input budget is just the window.\n\t\treturn longWindow > inputCap\n\t\t\t? { contextWindow: base, contextWindowOptions: [base, longWindow], maxInputTokens: inputCap }\n\t\t\t: { contextWindow: base, contextWindowOptions: [base, longWindow] };\n\t}\n\treturn { contextWindow: base };\n}\n\n/**\n * Parse a raw CAPI `/models` response body into an input-token context-window catalog.\n */\nexport function parseCopilotModelCatalog(body: unknown): CopilotModelCatalog {\n\tconst catalog = new Map<string, CopilotModelContext>();\n\tconst data = asRecord(body)?.data;\n\tif (!Array.isArray(data)) return catalog;\n\n\tfor (const entry of data) {\n\t\tconst record = asRecord(entry);\n\t\tif (!record) continue;\n\t\tconst id = record.id;\n\t\tif (typeof id !== \"string\" || id.length === 0) continue;\n\n\t\tconst limits = asRecord(asRecord(record.capabilities)?.limits);\n\t\tconst prices = asRecord(asRecord(record.billing)?.token_prices);\n\t\tconst context = resolveCopilotModelContext({\n\t\t\tmaxPromptTokens: toPositiveInt(limits?.max_prompt_tokens),\n\t\t\tmaxContextWindowTokens: toPositiveInt(limits?.max_context_window_tokens),\n\t\t\tmaxOutputTokens: toPositiveInt(limits?.max_output_tokens),\n\t\t\tdefaultContextMax: toPositiveInt(asRecord(prices?.default)?.context_max),\n\t\t\tlongContextMax: toPositiveInt(asRecord(prices?.long_context)?.context_max),\n\t\t});\n\t\tif (context) catalog.set(id, context);\n\t}\n\n\treturn catalog;\n}\n\nexport interface FetchCopilotModelCatalogOptions {\n\t/** Valid Copilot CAPI bearer token (e.g. from `modelRegistry.getApiKeyForProvider`). */\n\ttoken: string;\n\t/** Override the resolved base URL; defaults to one derived from the token. */\n\tbaseUrl?: string;\n\t/** Enterprise domain, used for base-URL resolution when the token lacks a `proxy-ep`. */\n\tenterpriseDomain?: string;\n\t/** Extra/override request headers. */\n\theaders?: Record<string, string>;\n\t/** Injectable `fetch` for testing. */\n\tfetchImpl?: typeof fetch;\n\t/** Abort signal. */\n\tsignal?: AbortSignal;\n}\n\n/** Fetch and parse the live Copilot model catalog from CAPI `GET {baseUrl}/models`. */\nexport async function fetchCopilotModelCatalog(options: FetchCopilotModelCatalogOptions): Promise<CopilotModelCatalog> {\n\tconst fetchImpl = options.fetchImpl ?? fetch;\n\tconst baseUrl = options.baseUrl ?? copilotApiBaseUrlFromToken(options.token, options.enterpriseDomain);\n\tconst response = await fetchImpl(`${trimTrailingSlash(baseUrl)}/models`, {\n\t\tmethod: \"GET\",\n\t\theaders: {\n\t\t\tAccept: \"application/json\",\n\t\t\tAuthorization: `Bearer ${options.token}`,\n\t\t\t...COPILOT_CATALOG_HEADERS,\n\t\t\t...options.headers,\n\t\t},\n\t\t...(options.signal ? { signal: options.signal } : {}),\n\t});\n\tif (!response.ok) {\n\t\tthrow new Error(`GitHub Copilot /models request failed: ${response.status} ${response.statusText}`);\n\t}\n\treturn parseCopilotModelCatalog(await response.json());\n}\n\n// ----------------------------------------------------------------------------\n// Active in-memory catalog (consulted by the model registry).\n//\n// Empty by default, so with no Copilot auth / no successful fetch the registry leaves Copilot\n// model context windows untouched and the picker never appears.\n// ----------------------------------------------------------------------------\n\nlet activeCatalog: CopilotModelCatalog = new Map();\n\n/** Replace the active catalog the registry derives context windows from. */\nexport function setActiveCopilotModelCatalog(catalog: CopilotModelCatalog): void {\n\tactiveCatalog = catalog;\n}\n\n/** The active catalog (empty until a successful auth-gated fetch/cache load). */\nexport function getActiveCopilotModelCatalog(): CopilotModelCatalog {\n\treturn activeCatalog;\n}\n\n/** Reset the active catalog (primarily for tests). */\nexport function clearActiveCopilotModelCatalog(): void {\n\tactiveCatalog = new Map();\n}\n\n// ----------------------------------------------------------------------------\n// Disk cache.\n// ----------------------------------------------------------------------------\n\ninterface CopilotCatalogCacheFile {\n\tversion: typeof COPILOT_CATALOG_CACHE_VERSION;\n\t/** CAPI host the catalog was fetched from; cache misses on host change (e.g. enterprise switch). */\n\thost: string;\n\t/** Epoch ms the catalog was fetched. */\n\tfetchedAt: number;\n\tmodels: Record<string, CopilotModelContext>;\n}\n\nfunction hostFromBaseUrl(baseUrl: string): string {\n\ttry {\n\t\treturn new URL(baseUrl).host;\n\t} catch {\n\t\treturn baseUrl;\n\t}\n}\n\nexport interface ReadCopilotCatalogCacheOptions {\n\t/** Expected CAPI host; a cached file from a different host is ignored. */\n\thost: string;\n\t/** Current epoch ms (injectable for tests). */\n\tnow?: number;\n\t/** Freshness window; defaults to {@link COPILOT_CATALOG_CACHE_TTL_MS}. */\n\tttlMs?: number;\n}\n\nfunction sanitizeCachedContext(value: unknown): CopilotModelContext | undefined {\n\tconst record = asRecord(value);\n\tconst contextWindow = toPositiveInt(record?.contextWindow);\n\tif (contextWindow === undefined) return undefined;\n\tconst maxInputTokens = toPositiveInt(record?.maxInputTokens);\n\tconst rawOptions = record?.contextWindowOptions;\n\tif (Array.isArray(rawOptions)) {\n\t\tconst options = rawOptions.map(toPositiveInt).filter((n): n is number => n !== undefined);\n\t\tif (options.length > 1) {\n\t\t\treturn maxInputTokens !== undefined\n\t\t\t\t? { contextWindow, contextWindowOptions: options, maxInputTokens }\n\t\t\t\t: { contextWindow, contextWindowOptions: options };\n\t\t}\n\t}\n\treturn maxInputTokens !== undefined ? { contextWindow, maxInputTokens } : { contextWindow };\n}\n\n/** Read a fresh, host-matching catalog from the cache file, or `undefined` if missing/stale/invalid. */\nexport function readCopilotCatalogCache(\n\tpath: string,\n\toptions: ReadCopilotCatalogCacheOptions,\n): CopilotModelCatalog | undefined {\n\tlet parsed: CopilotCatalogCacheFile;\n\ttry {\n\t\tif (!existsSync(path)) return undefined;\n\t\tparsed = JSON.parse(readFileSync(path, \"utf8\")) as CopilotCatalogCacheFile;\n\t} catch {\n\t\treturn undefined;\n\t}\n\tif (!parsed || parsed.version !== COPILOT_CATALOG_CACHE_VERSION) return undefined;\n\tif (parsed.host !== options.host) return undefined;\n\tconst now = options.now ?? Date.now();\n\tconst ttlMs = options.ttlMs ?? COPILOT_CATALOG_CACHE_TTL_MS;\n\tif (typeof parsed.fetchedAt !== \"number\" || now - parsed.fetchedAt >= ttlMs) return undefined;\n\tconst models = asRecord(parsed.models);\n\tif (!models) return undefined;\n\n\tconst catalog = new Map<string, CopilotModelContext>();\n\tfor (const [id, value] of Object.entries(models)) {\n\t\tconst context = sanitizeCachedContext(value);\n\t\tif (context) catalog.set(id, context);\n\t}\n\treturn catalog;\n}\n\n/** Write the catalog to the cache file (creating parent dirs). Best-effort; never throws. */\nexport function writeCopilotCatalogCache(\n\tpath: string,\n\tbaseUrl: string,\n\tcatalog: CopilotModelCatalog,\n\tnow?: number,\n): void {\n\tconst payload: CopilotCatalogCacheFile = {\n\t\tversion: COPILOT_CATALOG_CACHE_VERSION,\n\t\thost: hostFromBaseUrl(baseUrl),\n\t\tfetchedAt: now ?? Date.now(),\n\t\tmodels: Object.fromEntries(catalog),\n\t};\n\ttry {\n\t\tmkdirSync(dirname(path), { recursive: true });\n\t\twriteFileSync(path, JSON.stringify(payload), \"utf8\");\n\t} catch {\n\t\t// best-effort cache; ignore write failures\n\t}\n}\n\n/** Host component of a base URL, for matching {@link readCopilotCatalogCache} `host`. */\nexport function copilotCatalogCacheHost(baseUrl: string): string {\n\treturn hostFromBaseUrl(baseUrl);\n}\n\n/** Standard on-disk cache path for the Copilot model catalog under an agent directory. */\nexport function copilotCatalogCachePath(agentDir: string): string {\n\treturn join(agentDir, \"cache\", \"copilot-models.json\");\n}\n\n/**\n * Seed the active catalog synchronously from the on-disk cache, gated on a Copilot access token.\n *\n * Called at model-registry construction so a returning user's previously selected long-context\n * window is recognized before startup validation runs — otherwise the persisted choice would warn\n * (\"context window 936k is not supported…\") and reset until the async refresh completes. The cache\n * TTL is intentionally ignored here: stale-but-present windows are still valid for selection, and\n * the async loader independently refetches on its own freshness window. Returns true when a catalog\n * was applied. No-op (returns false) without a token or a host-matching cached catalog.\n */\nexport function seedActiveCopilotModelCatalogFromCache(\n\taccessToken: string | undefined,\n\tcachePath: string,\n\tnow?: number,\n): boolean {\n\tif (typeof accessToken !== \"string\" || accessToken.length === 0) return false;\n\tconst host = copilotCatalogCacheHost(copilotApiBaseUrlFromToken(accessToken));\n\tconst cached = readCopilotCatalogCache(cachePath, { host, now, ttlMs: Number.POSITIVE_INFINITY });\n\tif (!cached) return false;\n\tsetActiveCopilotModelCatalog(cached);\n\treturn true;\n}\n"]}
@@ -1,3 +1,4 @@
1
+ import * as undici from "undici";
1
2
  export declare const DEFAULT_HTTP_IDLE_TIMEOUT_MS = 600000;
2
3
  export declare const HTTP_IDLE_TIMEOUT_CHOICES: readonly [{
3
4
  readonly label: "30 sec";
@@ -20,6 +21,7 @@ export declare const HTTP_IDLE_TIMEOUT_CHOICES: readonly [{
20
21
  }];
21
22
  export declare function parseHttpIdleTimeoutMs(value: unknown): number | undefined;
22
23
  export declare function formatHttpIdleTimeoutMs(timeoutMs: number): string;
24
+ export declare function createHttpDispatcherOptions(timeoutMs: number): ConstructorParameters<typeof undici.EnvHttpProxyAgent>[0];
23
25
  /**
24
26
  * Configure the global undici dispatcher used by fetch and SDK HTTP clients.
25
27
  *
@@ -1 +1 @@
1
- {"version":3,"file":"http-dispatcher.d.ts","sourceRoot":"","sources":["../../src/core/http-dispatcher.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,4BAA4B,SAAU,CAAC;AAKpD,eAAO,MAAM,yBAAyB;aACnC,KAAK,EAAE,QAAQ;aAAE,SAAS,EAAE,KAAM;;aAClC,KAAK,EAAE,OAAO;aAAE,SAAS,EAAE,KAAM;;aACjC,KAAK,EAAE,OAAO;aAAE,SAAS,EAAE,MAAO;;aAClC,KAAK,EAAE,QAAQ;aAAE,SAAS,EAAE,MAAO;;aACnC,KAAK,EAAE,QAAQ;aAAE,SAAS,EAAE,OAAS;;aACrC,KAAK,EAAE,UAAU;aAAE,SAAS,EAAE,CAAC;EACxB,CAAC;AAEX,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAKzE;AAED,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAMjE;AAED;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,CAAC,SAAS,GAAE,MAAqC,GAAG,IAAI,CA+B9F","sourcesContent":["import * as undici from \"undici\";\nimport { installCopilotGeminiReasoningInterceptor } from \"./copilot-gemini-reasoning.ts\";\n\nexport const DEFAULT_HTTP_IDLE_TIMEOUT_MS = 600_000;\n\nconst originalGlobalFetch = globalThis.fetch;\nlet installedGlobalFetch: typeof globalThis.fetch | undefined;\n\nexport const HTTP_IDLE_TIMEOUT_CHOICES = [\n\t{ label: \"30 sec\", timeoutMs: 30_000 },\n\t{ label: \"1 min\", timeoutMs: 60_000 },\n\t{ label: \"5 min\", timeoutMs: 300_000 },\n\t{ label: \"10 min\", timeoutMs: 600_000 },\n\t{ label: \"30 min\", timeoutMs: 1_800_000 },\n\t{ label: \"Disabled\", timeoutMs: 0 },\n] as const;\n\nexport function parseHttpIdleTimeoutMs(value: unknown): number | undefined {\n\tif (typeof value !== \"number\" || !Number.isFinite(value) || value < 0) {\n\t\treturn undefined;\n\t}\n\treturn Math.floor(value);\n}\n\nexport function formatHttpIdleTimeoutMs(timeoutMs: number): string {\n\tconst choice = HTTP_IDLE_TIMEOUT_CHOICES.find((item) => item.timeoutMs === timeoutMs);\n\tif (choice) {\n\t\treturn choice.label;\n\t}\n\treturn `${timeoutMs / 1000} sec`;\n}\n\n/**\n * Configure the global undici dispatcher used by fetch and SDK HTTP clients.\n *\n * Keep HTTP/2 disabled for now because some Node/undici combinations have\n * produced stream-reset crashes, and use a configurable idle timeout so stale\n * connections are eventually reclaimed while long-running requests remain\n * supported. Do not install a fixed connect-phase timeout here: under Pier and\n * other policy/proxy layers, CONNECT establishment can be slower than normal\n * internet egress and should surface through the provider/agent retry path.\n */\nexport function configureHttpDispatcher(timeoutMs: number = DEFAULT_HTTP_IDLE_TIMEOUT_MS): void {\n\tconst normalizedTimeoutMs = parseHttpIdleTimeoutMs(timeoutMs);\n\tif (normalizedTimeoutMs === undefined) {\n\t\tthrow new Error(`Invalid HTTP idle timeout: ${String(timeoutMs)}`);\n\t}\n\n\tundici.setGlobalDispatcher(\n\t\tnew undici.EnvHttpProxyAgent({\n\t\t\tallowH2: false,\n\t\t\tbodyTimeout: normalizedTimeoutMs,\n\t\t\theadersTimeout: normalizedTimeoutMs,\n\t\t}),\n\t);\n\n\t// Keep fetch and the dispatcher on the same undici implementation. Some Node\n\t// releases use a bundled fetch that can ignore the npm undici dispatcher or\n\t// otherwise behave differently from the configured dispatcher used by SDKs.\n\t// If a caller replaced fetch after module load, preserve that deliberate\n\t// override.\n\tconst shouldInstallGlobals = installedGlobalFetch === undefined\n\t\t? globalThis.fetch === originalGlobalFetch\n\t\t: globalThis.fetch === installedGlobalFetch;\n\tif (shouldInstallGlobals) {\n\t\tundici.install?.();\n\t\tinstalledGlobalFetch = globalThis.fetch;\n\t}\n\n\t// Bridge CAPI Gemini thought signatures (`reasoning_opaque`) on the inbound\n\t// SSE stream so multi-turn Copilot Gemini tool use does not stall on empty\n\t// completions. Idempotent and scoped to `*.githubcopilot.com` event streams.\n\tinstallCopilotGeminiReasoningInterceptor();\n}\n"]}
1
+ {"version":3,"file":"http-dispatcher.d.ts","sourceRoot":"","sources":["../../src/core/http-dispatcher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AAGjC,eAAO,MAAM,4BAA4B,SAAU,CAAC;AAKpD,eAAO,MAAM,yBAAyB;aACnC,KAAK,EAAE,QAAQ;aAAE,SAAS,EAAE,KAAM;;aAClC,KAAK,EAAE,OAAO;aAAE,SAAS,EAAE,KAAM;;aACjC,KAAK,EAAE,OAAO;aAAE,SAAS,EAAE,MAAO;;aAClC,KAAK,EAAE,QAAQ;aAAE,SAAS,EAAE,MAAO;;aACnC,KAAK,EAAE,QAAQ;aAAE,SAAS,EAAE,OAAS;;aACrC,KAAK,EAAE,UAAU;aAAE,SAAS,EAAE,CAAC;EACxB,CAAC;AAEX,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAKzE;AAED,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAMjE;AAED,wBAAgB,2BAA2B,CAAC,SAAS,EAAE,MAAM,GAAG,qBAAqB,CAAC,OAAO,MAAM,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,CASxH;AAED;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,CAAC,SAAS,GAAE,MAAqC,GAAG,IAAI,CA2B9F","sourcesContent":["import * as undici from \"undici\";\nimport { installCopilotGeminiReasoningInterceptor } from \"./copilot-gemini-reasoning.ts\";\n\nexport const DEFAULT_HTTP_IDLE_TIMEOUT_MS = 600_000;\n\nconst originalGlobalFetch = globalThis.fetch;\nlet installedGlobalFetch: typeof globalThis.fetch | undefined;\n\nexport const HTTP_IDLE_TIMEOUT_CHOICES = [\n\t{ label: \"30 sec\", timeoutMs: 30_000 },\n\t{ label: \"1 min\", timeoutMs: 60_000 },\n\t{ label: \"5 min\", timeoutMs: 300_000 },\n\t{ label: \"10 min\", timeoutMs: 600_000 },\n\t{ label: \"30 min\", timeoutMs: 1_800_000 },\n\t{ label: \"Disabled\", timeoutMs: 0 },\n] as const;\n\nexport function parseHttpIdleTimeoutMs(value: unknown): number | undefined {\n\tif (typeof value !== \"number\" || !Number.isFinite(value) || value < 0) {\n\t\treturn undefined;\n\t}\n\treturn Math.floor(value);\n}\n\nexport function formatHttpIdleTimeoutMs(timeoutMs: number): string {\n\tconst choice = HTTP_IDLE_TIMEOUT_CHOICES.find((item) => item.timeoutMs === timeoutMs);\n\tif (choice) {\n\t\treturn choice.label;\n\t}\n\treturn `${timeoutMs / 1000} sec`;\n}\n\nexport function createHttpDispatcherOptions(timeoutMs: number): ConstructorParameters<typeof undici.EnvHttpProxyAgent>[0] {\n\treturn {\n\t\tallowH2: false,\n\t\t// Undici defaults to a 10s connect timeout; disable it so slow\n\t\t// policy/proxy CONNECT establishment follows provider retry handling.\n\t\tconnectTimeout: 0,\n\t\tbodyTimeout: timeoutMs,\n\t\theadersTimeout: timeoutMs,\n\t};\n}\n\n/**\n * Configure the global undici dispatcher used by fetch and SDK HTTP clients.\n *\n * Keep HTTP/2 disabled for now because some Node/undici combinations have\n * produced stream-reset crashes, and use a configurable idle timeout so stale\n * connections are eventually reclaimed while long-running requests remain\n * supported. Do not install a fixed connect-phase timeout here: under Pier and\n * other policy/proxy layers, CONNECT establishment can be slower than normal\n * internet egress and should surface through the provider/agent retry path.\n */\nexport function configureHttpDispatcher(timeoutMs: number = DEFAULT_HTTP_IDLE_TIMEOUT_MS): void {\n\tconst normalizedTimeoutMs = parseHttpIdleTimeoutMs(timeoutMs);\n\tif (normalizedTimeoutMs === undefined) {\n\t\tthrow new Error(`Invalid HTTP idle timeout: ${String(timeoutMs)}`);\n\t}\n\n\tundici.setGlobalDispatcher(\n\t\tnew undici.EnvHttpProxyAgent(createHttpDispatcherOptions(normalizedTimeoutMs)),\n\t);\n\n\t// Keep fetch and the dispatcher on the same undici implementation. Some Node\n\t// releases use a bundled fetch that can ignore the npm undici dispatcher or\n\t// otherwise behave differently from the configured dispatcher used by SDKs.\n\t// If a caller replaced fetch after module load, preserve that deliberate\n\t// override.\n\tconst shouldInstallGlobals = installedGlobalFetch === undefined\n\t\t? globalThis.fetch === originalGlobalFetch\n\t\t: globalThis.fetch === installedGlobalFetch;\n\tif (shouldInstallGlobals) {\n\t\tundici.install?.();\n\t\tinstalledGlobalFetch = globalThis.fetch;\n\t}\n\n\t// Bridge CAPI Gemini thought signatures (`reasoning_opaque`) on the inbound\n\t// SSE stream so multi-turn Copilot Gemini tool use does not stall on empty\n\t// completions. Idempotent and scoped to `*.githubcopilot.com` event streams.\n\tinstallCopilotGeminiReasoningInterceptor();\n}\n"]}
@@ -24,6 +24,16 @@ export function formatHttpIdleTimeoutMs(timeoutMs) {
24
24
  }
25
25
  return `${timeoutMs / 1000} sec`;
26
26
  }
27
+ export function createHttpDispatcherOptions(timeoutMs) {
28
+ return {
29
+ allowH2: false,
30
+ // Undici defaults to a 10s connect timeout; disable it so slow
31
+ // policy/proxy CONNECT establishment follows provider retry handling.
32
+ connectTimeout: 0,
33
+ bodyTimeout: timeoutMs,
34
+ headersTimeout: timeoutMs,
35
+ };
36
+ }
27
37
  /**
28
38
  * Configure the global undici dispatcher used by fetch and SDK HTTP clients.
29
39
  *
@@ -39,11 +49,7 @@ export function configureHttpDispatcher(timeoutMs = DEFAULT_HTTP_IDLE_TIMEOUT_MS
39
49
  if (normalizedTimeoutMs === undefined) {
40
50
  throw new Error(`Invalid HTTP idle timeout: ${String(timeoutMs)}`);
41
51
  }
42
- undici.setGlobalDispatcher(new undici.EnvHttpProxyAgent({
43
- allowH2: false,
44
- bodyTimeout: normalizedTimeoutMs,
45
- headersTimeout: normalizedTimeoutMs,
46
- }));
52
+ undici.setGlobalDispatcher(new undici.EnvHttpProxyAgent(createHttpDispatcherOptions(normalizedTimeoutMs)));
47
53
  // Keep fetch and the dispatcher on the same undici implementation. Some Node
48
54
  // releases use a bundled fetch that can ignore the npm undici dispatcher or
49
55
  // otherwise behave differently from the configured dispatcher used by SDKs.
@@ -1 +1 @@
1
- {"version":3,"file":"http-dispatcher.js","sourceRoot":"","sources":["../../src/core/http-dispatcher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AACjC,OAAO,EAAE,wCAAwC,EAAE,MAAM,+BAA+B,CAAC;AAEzF,MAAM,CAAC,MAAM,4BAA4B,GAAG,OAAO,CAAC;AAEpD,MAAM,mBAAmB,GAAG,UAAU,CAAC,KAAK,CAAC;AAC7C,IAAI,oBAAyD,CAAC;AAE9D,MAAM,CAAC,MAAM,yBAAyB,GAAG;IACxC,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE;IACtC,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE;IACrC,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE;IACtC,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE;IACvC,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE;IACzC,EAAE,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,CAAC,EAAE;CAC1B,CAAC;AAEX,MAAM,UAAU,sBAAsB,CAAC,KAAc;IACpD,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;QACvE,OAAO,SAAS,CAAC;IAClB,CAAC;IACD,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;AAC1B,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,SAAiB;IACxD,MAAM,MAAM,GAAG,yBAAyB,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC;IACtF,IAAI,MAAM,EAAE,CAAC;QACZ,OAAO,MAAM,CAAC,KAAK,CAAC;IACrB,CAAC;IACD,OAAO,GAAG,SAAS,GAAG,IAAI,MAAM,CAAC;AAClC,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,uBAAuB,CAAC,SAAS,GAAW,4BAA4B;IACvF,MAAM,mBAAmB,GAAG,sBAAsB,CAAC,SAAS,CAAC,CAAC;IAC9D,IAAI,mBAAmB,KAAK,SAAS,EAAE,CAAC;QACvC,MAAM,IAAI,KAAK,CAAC,8BAA8B,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;IACpE,CAAC;IAED,MAAM,CAAC,mBAAmB,CACzB,IAAI,MAAM,CAAC,iBAAiB,CAAC;QAC5B,OAAO,EAAE,KAAK;QACd,WAAW,EAAE,mBAAmB;QAChC,cAAc,EAAE,mBAAmB;KACnC,CAAC,CACF,CAAC;IAEF,6EAA6E;IAC7E,4EAA4E;IAC5E,4EAA4E;IAC5E,yEAAyE;IACzE,YAAY;IACZ,MAAM,oBAAoB,GAAG,oBAAoB,KAAK,SAAS;QAC9D,CAAC,CAAC,UAAU,CAAC,KAAK,KAAK,mBAAmB;QAC1C,CAAC,CAAC,UAAU,CAAC,KAAK,KAAK,oBAAoB,CAAC;IAC7C,IAAI,oBAAoB,EAAE,CAAC;QAC1B,MAAM,CAAC,OAAO,EAAE,EAAE,CAAC;QACnB,oBAAoB,GAAG,UAAU,CAAC,KAAK,CAAC;IACzC,CAAC;IAED,4EAA4E;IAC5E,2EAA2E;IAC3E,6EAA6E;IAC7E,wCAAwC,EAAE,CAAC;AAC5C,CAAC","sourcesContent":["import * as undici from \"undici\";\nimport { installCopilotGeminiReasoningInterceptor } from \"./copilot-gemini-reasoning.ts\";\n\nexport const DEFAULT_HTTP_IDLE_TIMEOUT_MS = 600_000;\n\nconst originalGlobalFetch = globalThis.fetch;\nlet installedGlobalFetch: typeof globalThis.fetch | undefined;\n\nexport const HTTP_IDLE_TIMEOUT_CHOICES = [\n\t{ label: \"30 sec\", timeoutMs: 30_000 },\n\t{ label: \"1 min\", timeoutMs: 60_000 },\n\t{ label: \"5 min\", timeoutMs: 300_000 },\n\t{ label: \"10 min\", timeoutMs: 600_000 },\n\t{ label: \"30 min\", timeoutMs: 1_800_000 },\n\t{ label: \"Disabled\", timeoutMs: 0 },\n] as const;\n\nexport function parseHttpIdleTimeoutMs(value: unknown): number | undefined {\n\tif (typeof value !== \"number\" || !Number.isFinite(value) || value < 0) {\n\t\treturn undefined;\n\t}\n\treturn Math.floor(value);\n}\n\nexport function formatHttpIdleTimeoutMs(timeoutMs: number): string {\n\tconst choice = HTTP_IDLE_TIMEOUT_CHOICES.find((item) => item.timeoutMs === timeoutMs);\n\tif (choice) {\n\t\treturn choice.label;\n\t}\n\treturn `${timeoutMs / 1000} sec`;\n}\n\n/**\n * Configure the global undici dispatcher used by fetch and SDK HTTP clients.\n *\n * Keep HTTP/2 disabled for now because some Node/undici combinations have\n * produced stream-reset crashes, and use a configurable idle timeout so stale\n * connections are eventually reclaimed while long-running requests remain\n * supported. Do not install a fixed connect-phase timeout here: under Pier and\n * other policy/proxy layers, CONNECT establishment can be slower than normal\n * internet egress and should surface through the provider/agent retry path.\n */\nexport function configureHttpDispatcher(timeoutMs: number = DEFAULT_HTTP_IDLE_TIMEOUT_MS): void {\n\tconst normalizedTimeoutMs = parseHttpIdleTimeoutMs(timeoutMs);\n\tif (normalizedTimeoutMs === undefined) {\n\t\tthrow new Error(`Invalid HTTP idle timeout: ${String(timeoutMs)}`);\n\t}\n\n\tundici.setGlobalDispatcher(\n\t\tnew undici.EnvHttpProxyAgent({\n\t\t\tallowH2: false,\n\t\t\tbodyTimeout: normalizedTimeoutMs,\n\t\t\theadersTimeout: normalizedTimeoutMs,\n\t\t}),\n\t);\n\n\t// Keep fetch and the dispatcher on the same undici implementation. Some Node\n\t// releases use a bundled fetch that can ignore the npm undici dispatcher or\n\t// otherwise behave differently from the configured dispatcher used by SDKs.\n\t// If a caller replaced fetch after module load, preserve that deliberate\n\t// override.\n\tconst shouldInstallGlobals = installedGlobalFetch === undefined\n\t\t? globalThis.fetch === originalGlobalFetch\n\t\t: globalThis.fetch === installedGlobalFetch;\n\tif (shouldInstallGlobals) {\n\t\tundici.install?.();\n\t\tinstalledGlobalFetch = globalThis.fetch;\n\t}\n\n\t// Bridge CAPI Gemini thought signatures (`reasoning_opaque`) on the inbound\n\t// SSE stream so multi-turn Copilot Gemini tool use does not stall on empty\n\t// completions. Idempotent and scoped to `*.githubcopilot.com` event streams.\n\tinstallCopilotGeminiReasoningInterceptor();\n}\n"]}
1
+ {"version":3,"file":"http-dispatcher.js","sourceRoot":"","sources":["../../src/core/http-dispatcher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AACjC,OAAO,EAAE,wCAAwC,EAAE,MAAM,+BAA+B,CAAC;AAEzF,MAAM,CAAC,MAAM,4BAA4B,GAAG,OAAO,CAAC;AAEpD,MAAM,mBAAmB,GAAG,UAAU,CAAC,KAAK,CAAC;AAC7C,IAAI,oBAAyD,CAAC;AAE9D,MAAM,CAAC,MAAM,yBAAyB,GAAG;IACxC,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE;IACtC,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE;IACrC,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE;IACtC,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE;IACvC,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE;IACzC,EAAE,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,CAAC,EAAE;CAC1B,CAAC;AAEX,MAAM,UAAU,sBAAsB,CAAC,KAAc;IACpD,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;QACvE,OAAO,SAAS,CAAC;IAClB,CAAC;IACD,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;AAC1B,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,SAAiB;IACxD,MAAM,MAAM,GAAG,yBAAyB,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC;IACtF,IAAI,MAAM,EAAE,CAAC;QACZ,OAAO,MAAM,CAAC,KAAK,CAAC;IACrB,CAAC;IACD,OAAO,GAAG,SAAS,GAAG,IAAI,MAAM,CAAC;AAClC,CAAC;AAED,MAAM,UAAU,2BAA2B,CAAC,SAAiB;IAC5D,OAAO;QACN,OAAO,EAAE,KAAK;QACd,+DAA+D;QAC/D,sEAAsE;QACtE,cAAc,EAAE,CAAC;QACjB,WAAW,EAAE,SAAS;QACtB,cAAc,EAAE,SAAS;KACzB,CAAC;AACH,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,uBAAuB,CAAC,SAAS,GAAW,4BAA4B;IACvF,MAAM,mBAAmB,GAAG,sBAAsB,CAAC,SAAS,CAAC,CAAC;IAC9D,IAAI,mBAAmB,KAAK,SAAS,EAAE,CAAC;QACvC,MAAM,IAAI,KAAK,CAAC,8BAA8B,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;IACpE,CAAC;IAED,MAAM,CAAC,mBAAmB,CACzB,IAAI,MAAM,CAAC,iBAAiB,CAAC,2BAA2B,CAAC,mBAAmB,CAAC,CAAC,CAC9E,CAAC;IAEF,6EAA6E;IAC7E,4EAA4E;IAC5E,4EAA4E;IAC5E,yEAAyE;IACzE,YAAY;IACZ,MAAM,oBAAoB,GAAG,oBAAoB,KAAK,SAAS;QAC9D,CAAC,CAAC,UAAU,CAAC,KAAK,KAAK,mBAAmB;QAC1C,CAAC,CAAC,UAAU,CAAC,KAAK,KAAK,oBAAoB,CAAC;IAC7C,IAAI,oBAAoB,EAAE,CAAC;QAC1B,MAAM,CAAC,OAAO,EAAE,EAAE,CAAC;QACnB,oBAAoB,GAAG,UAAU,CAAC,KAAK,CAAC;IACzC,CAAC;IAED,4EAA4E;IAC5E,2EAA2E;IAC3E,6EAA6E;IAC7E,wCAAwC,EAAE,CAAC;AAC5C,CAAC","sourcesContent":["import * as undici from \"undici\";\nimport { installCopilotGeminiReasoningInterceptor } from \"./copilot-gemini-reasoning.ts\";\n\nexport const DEFAULT_HTTP_IDLE_TIMEOUT_MS = 600_000;\n\nconst originalGlobalFetch = globalThis.fetch;\nlet installedGlobalFetch: typeof globalThis.fetch | undefined;\n\nexport const HTTP_IDLE_TIMEOUT_CHOICES = [\n\t{ label: \"30 sec\", timeoutMs: 30_000 },\n\t{ label: \"1 min\", timeoutMs: 60_000 },\n\t{ label: \"5 min\", timeoutMs: 300_000 },\n\t{ label: \"10 min\", timeoutMs: 600_000 },\n\t{ label: \"30 min\", timeoutMs: 1_800_000 },\n\t{ label: \"Disabled\", timeoutMs: 0 },\n] as const;\n\nexport function parseHttpIdleTimeoutMs(value: unknown): number | undefined {\n\tif (typeof value !== \"number\" || !Number.isFinite(value) || value < 0) {\n\t\treturn undefined;\n\t}\n\treturn Math.floor(value);\n}\n\nexport function formatHttpIdleTimeoutMs(timeoutMs: number): string {\n\tconst choice = HTTP_IDLE_TIMEOUT_CHOICES.find((item) => item.timeoutMs === timeoutMs);\n\tif (choice) {\n\t\treturn choice.label;\n\t}\n\treturn `${timeoutMs / 1000} sec`;\n}\n\nexport function createHttpDispatcherOptions(timeoutMs: number): ConstructorParameters<typeof undici.EnvHttpProxyAgent>[0] {\n\treturn {\n\t\tallowH2: false,\n\t\t// Undici defaults to a 10s connect timeout; disable it so slow\n\t\t// policy/proxy CONNECT establishment follows provider retry handling.\n\t\tconnectTimeout: 0,\n\t\tbodyTimeout: timeoutMs,\n\t\theadersTimeout: timeoutMs,\n\t};\n}\n\n/**\n * Configure the global undici dispatcher used by fetch and SDK HTTP clients.\n *\n * Keep HTTP/2 disabled for now because some Node/undici combinations have\n * produced stream-reset crashes, and use a configurable idle timeout so stale\n * connections are eventually reclaimed while long-running requests remain\n * supported. Do not install a fixed connect-phase timeout here: under Pier and\n * other policy/proxy layers, CONNECT establishment can be slower than normal\n * internet egress and should surface through the provider/agent retry path.\n */\nexport function configureHttpDispatcher(timeoutMs: number = DEFAULT_HTTP_IDLE_TIMEOUT_MS): void {\n\tconst normalizedTimeoutMs = parseHttpIdleTimeoutMs(timeoutMs);\n\tif (normalizedTimeoutMs === undefined) {\n\t\tthrow new Error(`Invalid HTTP idle timeout: ${String(timeoutMs)}`);\n\t}\n\n\tundici.setGlobalDispatcher(\n\t\tnew undici.EnvHttpProxyAgent(createHttpDispatcherOptions(normalizedTimeoutMs)),\n\t);\n\n\t// Keep fetch and the dispatcher on the same undici implementation. Some Node\n\t// releases use a bundled fetch that can ignore the npm undici dispatcher or\n\t// otherwise behave differently from the configured dispatcher used by SDKs.\n\t// If a caller replaced fetch after module load, preserve that deliberate\n\t// override.\n\tconst shouldInstallGlobals = installedGlobalFetch === undefined\n\t\t? globalThis.fetch === originalGlobalFetch\n\t\t: globalThis.fetch === installedGlobalFetch;\n\tif (shouldInstallGlobals) {\n\t\tundici.install?.();\n\t\tinstalledGlobalFetch = globalThis.fetch;\n\t}\n\n\t// Bridge CAPI Gemini thought signatures (`reasoning_opaque`) on the inbound\n\t// SSE stream so multi-turn Copilot Gemini tool use does not stall on empty\n\t// completions. Idempotent and scoped to `*.githubcopilot.com` event streams.\n\tinstallCopilotGeminiReasoningInterceptor();\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"model-registry-builtins.d.ts","sourceRoot":"","sources":["../../src/core/model-registry-builtins.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,GAAG,EAIR,KAAK,KAAK,EAEV,MAAM,8BAA8B,CAAC;AAGtC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,KAAK,EAAkB,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAWlF,wBAAgB,iCAAiC,CAChD,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,EACjB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,GACzC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAKpC;AAaD,wBAAgB,WAAW,CAC1B,UAAU,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,EAChC,cAAc,EAAE,aAAa,CAAC,QAAQ,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,GAC5D,KAAK,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,GAAG,SAAS,CAiClC;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,QAAQ,EAAE,aAAa,GAAG,KAAK,CAAC,GAAG,CAAC,CAgCzF;AAED,wBAAgB,iBAAiB,CAChC,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,gBAAgB,CAAC,EACxC,cAAc,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,GACrD,KAAK,CAAC,GAAG,CAAC,EAAE,CAsBd;AAED,wBAAgB,iBAAiB,CAAC,aAAa,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,EAAE,YAAY,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,GAAG,KAAK,CAAC,GAAG,CAAC,EAAE,CAWvG","sourcesContent":["import {\n\ttype Api,\n\tgetModels,\n\tgetProviders,\n\ttype KnownProvider,\n\ttype Model,\n\ttype OpenAICompletionsCompat,\n} from \"@earendil-works/pi-ai/compat\";\nimport { normalizeContextWindowOptions, withContextWindowOptions } from \"./context-window.ts\";\nimport { getActiveCopilotModelCatalog } from \"./copilot-model-catalog.ts\";\nimport type { ModelOverride } from \"./model-registry-schemas.ts\";\nimport type { ProviderCompat, ProviderOverride } from \"./model-registry-types.ts\";\n\nconst GITHUB_COPILOT_API_VERSION_HEADER = \"X-GitHub-Api-Version\";\nconst GITHUB_COPILOT_API_VERSION = \"2026-06-01\";\n\nfunction hasHeader(headers: Record<string, string> | undefined, headerName: string): boolean {\n\tif (!headers) return false;\n\tconst normalizedHeaderName = headerName.toLowerCase();\n\treturn Object.keys(headers).some((key) => key.toLowerCase() === normalizedHeaderName);\n}\n\nexport function withGitHubCopilotApiVersionHeader(\n\tmodel: Model<Api>,\n\theaders: Record<string, string> | undefined,\n): Record<string, string> | undefined {\n\tif (model.provider !== \"github-copilot\" || hasHeader(headers, GITHUB_COPILOT_API_VERSION_HEADER)) {\n\t\treturn headers;\n\t}\n\treturn { ...(headers ?? {}), [GITHUB_COPILOT_API_VERSION_HEADER]: GITHUB_COPILOT_API_VERSION };\n}\n\nfunction withCopilotContextWindowOptions(model: Model<Api>): Model<Api> {\n\tif (model.provider !== \"github-copilot\") return model;\n\tconst context = getActiveCopilotModelCatalog().get(model.id);\n\tif (!context) return model;\n\tconst base = { ...model, contextWindow: context.contextWindow, maxInputTokens: context.maxInputTokens };\n\tif (context.contextWindowOptions && context.contextWindowOptions.length > 1) {\n\t\treturn withContextWindowOptions(base, context.contextWindowOptions);\n\t}\n\treturn base;\n}\n\nexport function mergeCompat(\n\tbaseCompat: Model<Api>[\"compat\"],\n\toverrideCompat: ModelOverride[\"compat\"] | Model<Api>[\"compat\"],\n): Model<Api>[\"compat\"] | undefined {\n\tif (!overrideCompat) return baseCompat;\n\n\tconst base = baseCompat as ProviderCompat | undefined;\n\tconst override = overrideCompat as ProviderCompat;\n\tconst merged = { ...base, ...override } as ProviderCompat;\n\n\tconst baseCompletions = base as OpenAICompletionsCompat | undefined;\n\tconst overrideCompletions = override as OpenAICompletionsCompat;\n\tconst mergedCompletions = merged as OpenAICompletionsCompat;\n\n\tif (baseCompletions?.openRouterRouting || overrideCompletions.openRouterRouting) {\n\t\tmergedCompletions.openRouterRouting = {\n\t\t\t...baseCompletions?.openRouterRouting,\n\t\t\t...overrideCompletions.openRouterRouting,\n\t\t};\n\t}\n\n\tif (baseCompletions?.vercelGatewayRouting || overrideCompletions.vercelGatewayRouting) {\n\t\tmergedCompletions.vercelGatewayRouting = {\n\t\t\t...baseCompletions?.vercelGatewayRouting,\n\t\t\t...overrideCompletions.vercelGatewayRouting,\n\t\t};\n\t}\n\n\tif (baseCompletions?.chatTemplateKwargs || overrideCompletions.chatTemplateKwargs) {\n\t\tmergedCompletions.chatTemplateKwargs = {\n\t\t\t...baseCompletions?.chatTemplateKwargs,\n\t\t\t...overrideCompletions.chatTemplateKwargs,\n\t\t};\n\t}\n\n\treturn merged as Model<Api>[\"compat\"];\n}\n\nexport function applyModelOverride(model: Model<Api>, override: ModelOverride): Model<Api> {\n\tconst result = { ...model };\n\n\tif (override.name !== undefined) result.name = override.name;\n\tif (override.reasoning !== undefined) result.reasoning = override.reasoning;\n\tif (override.thinkingLevelMap !== undefined) {\n\t\tresult.thinkingLevelMap = { ...model.thinkingLevelMap, ...override.thinkingLevelMap };\n\t}\n\tif (override.input !== undefined) result.input = override.input as (\"text\" | \"image\")[];\n\tif (override.contextWindow !== undefined) {\n\t\tresult.contextWindow = override.contextWindow;\n\t\tresult.defaultContextWindow = override.contextWindow;\n\t\tif (override.contextWindowOptions === undefined) {\n\t\t\tresult.contextWindowOptions = undefined;\n\t\t}\n\t}\n\tif (override.contextWindowOptions !== undefined) {\n\t\tresult.contextWindowOptions = normalizeContextWindowOptions(override.contextWindowOptions);\n\t}\n\tif (override.maxTokens !== undefined) result.maxTokens = override.maxTokens;\n\n\tif (override.cost) {\n\t\tresult.cost = {\n\t\t\tinput: override.cost.input ?? model.cost.input,\n\t\t\toutput: override.cost.output ?? model.cost.output,\n\t\t\tcacheRead: override.cost.cacheRead ?? model.cost.cacheRead,\n\t\t\tcacheWrite: override.cost.cacheWrite ?? model.cost.cacheWrite,\n\t\t};\n\t}\n\n\tresult.compat = mergeCompat(model.compat, override.compat);\n\treturn result;\n}\n\nexport function loadBuiltInModels(\n\toverrides: Map<string, ProviderOverride>,\n\tmodelOverrides: Map<string, Map<string, ModelOverride>>,\n): Model<Api>[] {\n\treturn getProviders().flatMap((provider) => {\n\t\tconst models = getModels(provider as KnownProvider) as Model<Api>[];\n\t\tconst providerOverride = overrides.get(provider);\n\t\tconst perModelOverrides = modelOverrides.get(provider);\n\n\t\treturn models.map((m) => {\n\t\t\tlet model = m;\n\n\t\t\tif (providerOverride) {\n\t\t\t\tmodel = {\n\t\t\t\t\t...model,\n\t\t\t\t\tbaseUrl: providerOverride.baseUrl ?? model.baseUrl,\n\t\t\t\t\tcompat: mergeCompat(model.compat, providerOverride.compat),\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tmodel = withCopilotContextWindowOptions(model);\n\t\t\tconst modelOverride = perModelOverrides?.get(m.id);\n\t\t\treturn modelOverride ? applyModelOverride(model, modelOverride) : model;\n\t\t});\n\t});\n}\n\nexport function mergeCustomModels(builtInModels: Model<Api>[], customModels: Model<Api>[]): Model<Api>[] {\n\tconst merged = [...builtInModels];\n\tfor (const customModel of customModels) {\n\t\tconst existingIndex = merged.findIndex((m) => m.provider === customModel.provider && m.id === customModel.id);\n\t\tif (existingIndex >= 0) {\n\t\t\tmerged[existingIndex] = customModel;\n\t\t} else {\n\t\t\tmerged.push(customModel);\n\t\t}\n\t}\n\treturn merged;\n}\n"]}
1
+ {"version":3,"file":"model-registry-builtins.d.ts","sourceRoot":"","sources":["../../src/core/model-registry-builtins.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,GAAG,EAIR,KAAK,KAAK,EAEV,MAAM,8BAA8B,CAAC;AAGtC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,KAAK,EAAkB,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAWlF,wBAAgB,iCAAiC,CAChD,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,EACjB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,GACzC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAKpC;AAoBD,wBAAgB,WAAW,CAC1B,UAAU,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,EAChC,cAAc,EAAE,aAAa,CAAC,QAAQ,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,GAC5D,KAAK,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,GAAG,SAAS,CAiClC;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,QAAQ,EAAE,aAAa,GAAG,KAAK,CAAC,GAAG,CAAC,CAgCzF;AAED,wBAAgB,iBAAiB,CAChC,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,gBAAgB,CAAC,EACxC,cAAc,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,GACrD,KAAK,CAAC,GAAG,CAAC,EAAE,CAsBd;AAED,wBAAgB,iBAAiB,CAAC,aAAa,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,EAAE,YAAY,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,GAAG,KAAK,CAAC,GAAG,CAAC,EAAE,CAWvG","sourcesContent":["import {\n\ttype Api,\n\tgetModels,\n\tgetProviders,\n\ttype KnownProvider,\n\ttype Model,\n\ttype OpenAICompletionsCompat,\n} from \"@earendil-works/pi-ai/compat\";\nimport { normalizeContextWindowOptions, withContextWindowOptions } from \"./context-window.ts\";\nimport { copilotApiBaseUrlFromToken, copilotTokenFromEnvironment, DEFAULT_COPILOT_API_BASE_URL, getActiveCopilotModelCatalog } from \"./copilot-model-catalog.ts\";\nimport type { ModelOverride } from \"./model-registry-schemas.ts\";\nimport type { ProviderCompat, ProviderOverride } from \"./model-registry-types.ts\";\n\nconst GITHUB_COPILOT_API_VERSION_HEADER = \"X-GitHub-Api-Version\";\nconst GITHUB_COPILOT_API_VERSION = \"2026-06-01\";\n\nfunction hasHeader(headers: Record<string, string> | undefined, headerName: string): boolean {\n\tif (!headers) return false;\n\tconst normalizedHeaderName = headerName.toLowerCase();\n\treturn Object.keys(headers).some((key) => key.toLowerCase() === normalizedHeaderName);\n}\n\nexport function withGitHubCopilotApiVersionHeader(\n\tmodel: Model<Api>,\n\theaders: Record<string, string> | undefined,\n): Record<string, string> | undefined {\n\tif (model.provider !== \"github-copilot\" || hasHeader(headers, GITHUB_COPILOT_API_VERSION_HEADER)) {\n\t\treturn headers;\n\t}\n\treturn { ...(headers ?? {}), [GITHUB_COPILOT_API_VERSION_HEADER]: GITHUB_COPILOT_API_VERSION };\n}\n\nfunction withCopilotEnvironmentBaseUrl(model: Model<Api>): Model<Api> {\n\tif (model.provider !== \"github-copilot\") return model;\n\tconst resolvedBaseUrl = copilotApiBaseUrlFromToken(copilotTokenFromEnvironment());\n\tif (resolvedBaseUrl === DEFAULT_COPILOT_API_BASE_URL || resolvedBaseUrl === model.baseUrl) return model;\n\treturn { ...model, baseUrl: resolvedBaseUrl };\n}\n\nfunction withCopilotContextWindowOptions(model: Model<Api>): Model<Api> {\n\tif (model.provider !== \"github-copilot\") return model;\n\tconst context = getActiveCopilotModelCatalog().get(model.id);\n\tif (!context) return model;\n\tconst base = { ...model, contextWindow: context.contextWindow, maxInputTokens: context.maxInputTokens };\n\tif (context.contextWindowOptions && context.contextWindowOptions.length > 1) {\n\t\treturn withContextWindowOptions(base, context.contextWindowOptions);\n\t}\n\treturn base;\n}\n\nexport function mergeCompat(\n\tbaseCompat: Model<Api>[\"compat\"],\n\toverrideCompat: ModelOverride[\"compat\"] | Model<Api>[\"compat\"],\n): Model<Api>[\"compat\"] | undefined {\n\tif (!overrideCompat) return baseCompat;\n\n\tconst base = baseCompat as ProviderCompat | undefined;\n\tconst override = overrideCompat as ProviderCompat;\n\tconst merged = { ...base, ...override } as ProviderCompat;\n\n\tconst baseCompletions = base as OpenAICompletionsCompat | undefined;\n\tconst overrideCompletions = override as OpenAICompletionsCompat;\n\tconst mergedCompletions = merged as OpenAICompletionsCompat;\n\n\tif (baseCompletions?.openRouterRouting || overrideCompletions.openRouterRouting) {\n\t\tmergedCompletions.openRouterRouting = {\n\t\t\t...baseCompletions?.openRouterRouting,\n\t\t\t...overrideCompletions.openRouterRouting,\n\t\t};\n\t}\n\n\tif (baseCompletions?.vercelGatewayRouting || overrideCompletions.vercelGatewayRouting) {\n\t\tmergedCompletions.vercelGatewayRouting = {\n\t\t\t...baseCompletions?.vercelGatewayRouting,\n\t\t\t...overrideCompletions.vercelGatewayRouting,\n\t\t};\n\t}\n\n\tif (baseCompletions?.chatTemplateKwargs || overrideCompletions.chatTemplateKwargs) {\n\t\tmergedCompletions.chatTemplateKwargs = {\n\t\t\t...baseCompletions?.chatTemplateKwargs,\n\t\t\t...overrideCompletions.chatTemplateKwargs,\n\t\t};\n\t}\n\n\treturn merged as Model<Api>[\"compat\"];\n}\n\nexport function applyModelOverride(model: Model<Api>, override: ModelOverride): Model<Api> {\n\tconst result = { ...model };\n\n\tif (override.name !== undefined) result.name = override.name;\n\tif (override.reasoning !== undefined) result.reasoning = override.reasoning;\n\tif (override.thinkingLevelMap !== undefined) {\n\t\tresult.thinkingLevelMap = { ...model.thinkingLevelMap, ...override.thinkingLevelMap };\n\t}\n\tif (override.input !== undefined) result.input = override.input as (\"text\" | \"image\")[];\n\tif (override.contextWindow !== undefined) {\n\t\tresult.contextWindow = override.contextWindow;\n\t\tresult.defaultContextWindow = override.contextWindow;\n\t\tif (override.contextWindowOptions === undefined) {\n\t\t\tresult.contextWindowOptions = undefined;\n\t\t}\n\t}\n\tif (override.contextWindowOptions !== undefined) {\n\t\tresult.contextWindowOptions = normalizeContextWindowOptions(override.contextWindowOptions);\n\t}\n\tif (override.maxTokens !== undefined) result.maxTokens = override.maxTokens;\n\n\tif (override.cost) {\n\t\tresult.cost = {\n\t\t\tinput: override.cost.input ?? model.cost.input,\n\t\t\toutput: override.cost.output ?? model.cost.output,\n\t\t\tcacheRead: override.cost.cacheRead ?? model.cost.cacheRead,\n\t\t\tcacheWrite: override.cost.cacheWrite ?? model.cost.cacheWrite,\n\t\t};\n\t}\n\n\tresult.compat = mergeCompat(model.compat, override.compat);\n\treturn result;\n}\n\nexport function loadBuiltInModels(\n\toverrides: Map<string, ProviderOverride>,\n\tmodelOverrides: Map<string, Map<string, ModelOverride>>,\n): Model<Api>[] {\n\treturn getProviders().flatMap((provider) => {\n\t\tconst models = getModels(provider as KnownProvider) as Model<Api>[];\n\t\tconst providerOverride = overrides.get(provider);\n\t\tconst perModelOverrides = modelOverrides.get(provider);\n\n\t\treturn models.map((m) => {\n\t\t\tlet model = withCopilotEnvironmentBaseUrl(m);\n\n\t\t\tif (providerOverride) {\n\t\t\t\tmodel = {\n\t\t\t\t\t...model,\n\t\t\t\t\tbaseUrl: providerOverride.baseUrl ?? model.baseUrl,\n\t\t\t\t\tcompat: mergeCompat(model.compat, providerOverride.compat),\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tmodel = withCopilotContextWindowOptions(model);\n\t\t\tconst modelOverride = perModelOverrides?.get(m.id);\n\t\t\treturn modelOverride ? applyModelOverride(model, modelOverride) : model;\n\t\t});\n\t});\n}\n\nexport function mergeCustomModels(builtInModels: Model<Api>[], customModels: Model<Api>[]): Model<Api>[] {\n\tconst merged = [...builtInModels];\n\tfor (const customModel of customModels) {\n\t\tconst existingIndex = merged.findIndex((m) => m.provider === customModel.provider && m.id === customModel.id);\n\t\tif (existingIndex >= 0) {\n\t\t\tmerged[existingIndex] = customModel;\n\t\t} else {\n\t\t\tmerged.push(customModel);\n\t\t}\n\t}\n\treturn merged;\n}\n"]}
@@ -1,6 +1,6 @@
1
1
  import { getModels, getProviders, } from "@earendil-works/pi-ai/compat";
2
2
  import { normalizeContextWindowOptions, withContextWindowOptions } from "./context-window.js";
3
- import { getActiveCopilotModelCatalog } from "./copilot-model-catalog.js";
3
+ import { copilotApiBaseUrlFromToken, copilotTokenFromEnvironment, DEFAULT_COPILOT_API_BASE_URL, getActiveCopilotModelCatalog } from "./copilot-model-catalog.js";
4
4
  const GITHUB_COPILOT_API_VERSION_HEADER = "X-GitHub-Api-Version";
5
5
  const GITHUB_COPILOT_API_VERSION = "2026-06-01";
6
6
  function hasHeader(headers, headerName) {
@@ -15,6 +15,14 @@ export function withGitHubCopilotApiVersionHeader(model, headers) {
15
15
  }
16
16
  return { ...(headers ?? {}), [GITHUB_COPILOT_API_VERSION_HEADER]: GITHUB_COPILOT_API_VERSION };
17
17
  }
18
+ function withCopilotEnvironmentBaseUrl(model) {
19
+ if (model.provider !== "github-copilot")
20
+ return model;
21
+ const resolvedBaseUrl = copilotApiBaseUrlFromToken(copilotTokenFromEnvironment());
22
+ if (resolvedBaseUrl === DEFAULT_COPILOT_API_BASE_URL || resolvedBaseUrl === model.baseUrl)
23
+ return model;
24
+ return { ...model, baseUrl: resolvedBaseUrl };
25
+ }
18
26
  function withCopilotContextWindowOptions(model) {
19
27
  if (model.provider !== "github-copilot")
20
28
  return model;
@@ -96,7 +104,7 @@ export function loadBuiltInModels(overrides, modelOverrides) {
96
104
  const providerOverride = overrides.get(provider);
97
105
  const perModelOverrides = modelOverrides.get(provider);
98
106
  return models.map((m) => {
99
- let model = m;
107
+ let model = withCopilotEnvironmentBaseUrl(m);
100
108
  if (providerOverride) {
101
109
  model = {
102
110
  ...model,
@@ -1 +1 @@
1
- {"version":3,"file":"model-registry-builtins.js","sourceRoot":"","sources":["../../src/core/model-registry-builtins.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n\ttype Api,\n\tgetModels,\n\tgetProviders,\n\ttype KnownProvider,\n\ttype Model,\n\ttype OpenAICompletionsCompat,\n} from \"@earendil-works/pi-ai/compat\";\nimport { normalizeContextWindowOptions, withContextWindowOptions } from \"./context-window.ts\";\nimport { getActiveCopilotModelCatalog } from \"./copilot-model-catalog.ts\";\nimport type { ModelOverride } from \"./model-registry-schemas.ts\";\nimport type { ProviderCompat, ProviderOverride } from \"./model-registry-types.ts\";\n\nconst GITHUB_COPILOT_API_VERSION_HEADER = \"X-GitHub-Api-Version\";\nconst GITHUB_COPILOT_API_VERSION = \"2026-06-01\";\n\nfunction hasHeader(headers: Record<string, string> | undefined, headerName: string): boolean {\n\tif (!headers) return false;\n\tconst normalizedHeaderName = headerName.toLowerCase();\n\treturn Object.keys(headers).some((key) => key.toLowerCase() === normalizedHeaderName);\n}\n\nexport function withGitHubCopilotApiVersionHeader(\n\tmodel: Model<Api>,\n\theaders: Record<string, string> | undefined,\n): Record<string, string> | undefined {\n\tif (model.provider !== \"github-copilot\" || hasHeader(headers, GITHUB_COPILOT_API_VERSION_HEADER)) {\n\t\treturn headers;\n\t}\n\treturn { ...(headers ?? {}), [GITHUB_COPILOT_API_VERSION_HEADER]: GITHUB_COPILOT_API_VERSION };\n}\n\nfunction withCopilotContextWindowOptions(model: Model<Api>): Model<Api> {\n\tif (model.provider !== \"github-copilot\") return model;\n\tconst context = getActiveCopilotModelCatalog().get(model.id);\n\tif (!context) return model;\n\tconst base = { ...model, contextWindow: context.contextWindow, maxInputTokens: context.maxInputTokens };\n\tif (context.contextWindowOptions && context.contextWindowOptions.length > 1) {\n\t\treturn withContextWindowOptions(base, context.contextWindowOptions);\n\t}\n\treturn base;\n}\n\nexport function mergeCompat(\n\tbaseCompat: Model<Api>[\"compat\"],\n\toverrideCompat: ModelOverride[\"compat\"] | Model<Api>[\"compat\"],\n): Model<Api>[\"compat\"] | undefined {\n\tif (!overrideCompat) return baseCompat;\n\n\tconst base = baseCompat as ProviderCompat | undefined;\n\tconst override = overrideCompat as ProviderCompat;\n\tconst merged = { ...base, ...override } as ProviderCompat;\n\n\tconst baseCompletions = base as OpenAICompletionsCompat | undefined;\n\tconst overrideCompletions = override as OpenAICompletionsCompat;\n\tconst mergedCompletions = merged as OpenAICompletionsCompat;\n\n\tif (baseCompletions?.openRouterRouting || overrideCompletions.openRouterRouting) {\n\t\tmergedCompletions.openRouterRouting = {\n\t\t\t...baseCompletions?.openRouterRouting,\n\t\t\t...overrideCompletions.openRouterRouting,\n\t\t};\n\t}\n\n\tif (baseCompletions?.vercelGatewayRouting || overrideCompletions.vercelGatewayRouting) {\n\t\tmergedCompletions.vercelGatewayRouting = {\n\t\t\t...baseCompletions?.vercelGatewayRouting,\n\t\t\t...overrideCompletions.vercelGatewayRouting,\n\t\t};\n\t}\n\n\tif (baseCompletions?.chatTemplateKwargs || overrideCompletions.chatTemplateKwargs) {\n\t\tmergedCompletions.chatTemplateKwargs = {\n\t\t\t...baseCompletions?.chatTemplateKwargs,\n\t\t\t...overrideCompletions.chatTemplateKwargs,\n\t\t};\n\t}\n\n\treturn merged as Model<Api>[\"compat\"];\n}\n\nexport function applyModelOverride(model: Model<Api>, override: ModelOverride): Model<Api> {\n\tconst result = { ...model };\n\n\tif (override.name !== undefined) result.name = override.name;\n\tif (override.reasoning !== undefined) result.reasoning = override.reasoning;\n\tif (override.thinkingLevelMap !== undefined) {\n\t\tresult.thinkingLevelMap = { ...model.thinkingLevelMap, ...override.thinkingLevelMap };\n\t}\n\tif (override.input !== undefined) result.input = override.input as (\"text\" | \"image\")[];\n\tif (override.contextWindow !== undefined) {\n\t\tresult.contextWindow = override.contextWindow;\n\t\tresult.defaultContextWindow = override.contextWindow;\n\t\tif (override.contextWindowOptions === undefined) {\n\t\t\tresult.contextWindowOptions = undefined;\n\t\t}\n\t}\n\tif (override.contextWindowOptions !== undefined) {\n\t\tresult.contextWindowOptions = normalizeContextWindowOptions(override.contextWindowOptions);\n\t}\n\tif (override.maxTokens !== undefined) result.maxTokens = override.maxTokens;\n\n\tif (override.cost) {\n\t\tresult.cost = {\n\t\t\tinput: override.cost.input ?? model.cost.input,\n\t\t\toutput: override.cost.output ?? model.cost.output,\n\t\t\tcacheRead: override.cost.cacheRead ?? model.cost.cacheRead,\n\t\t\tcacheWrite: override.cost.cacheWrite ?? model.cost.cacheWrite,\n\t\t};\n\t}\n\n\tresult.compat = mergeCompat(model.compat, override.compat);\n\treturn result;\n}\n\nexport function loadBuiltInModels(\n\toverrides: Map<string, ProviderOverride>,\n\tmodelOverrides: Map<string, Map<string, ModelOverride>>,\n): Model<Api>[] {\n\treturn getProviders().flatMap((provider) => {\n\t\tconst models = getModels(provider as KnownProvider) as Model<Api>[];\n\t\tconst providerOverride = overrides.get(provider);\n\t\tconst perModelOverrides = modelOverrides.get(provider);\n\n\t\treturn models.map((m) => {\n\t\t\tlet model = m;\n\n\t\t\tif (providerOverride) {\n\t\t\t\tmodel = {\n\t\t\t\t\t...model,\n\t\t\t\t\tbaseUrl: providerOverride.baseUrl ?? model.baseUrl,\n\t\t\t\t\tcompat: mergeCompat(model.compat, providerOverride.compat),\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tmodel = withCopilotContextWindowOptions(model);\n\t\t\tconst modelOverride = perModelOverrides?.get(m.id);\n\t\t\treturn modelOverride ? applyModelOverride(model, modelOverride) : model;\n\t\t});\n\t});\n}\n\nexport function mergeCustomModels(builtInModels: Model<Api>[], customModels: Model<Api>[]): Model<Api>[] {\n\tconst merged = [...builtInModels];\n\tfor (const customModel of customModels) {\n\t\tconst existingIndex = merged.findIndex((m) => m.provider === customModel.provider && m.id === customModel.id);\n\t\tif (existingIndex >= 0) {\n\t\t\tmerged[existingIndex] = customModel;\n\t\t} else {\n\t\t\tmerged.push(customModel);\n\t\t}\n\t}\n\treturn merged;\n}\n"]}
1
+ {"version":3,"file":"model-registry-builtins.js","sourceRoot":"","sources":["../../src/core/model-registry-builtins.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n\ttype Api,\n\tgetModels,\n\tgetProviders,\n\ttype KnownProvider,\n\ttype Model,\n\ttype OpenAICompletionsCompat,\n} from \"@earendil-works/pi-ai/compat\";\nimport { normalizeContextWindowOptions, withContextWindowOptions } from \"./context-window.ts\";\nimport { copilotApiBaseUrlFromToken, copilotTokenFromEnvironment, DEFAULT_COPILOT_API_BASE_URL, getActiveCopilotModelCatalog } from \"./copilot-model-catalog.ts\";\nimport type { ModelOverride } from \"./model-registry-schemas.ts\";\nimport type { ProviderCompat, ProviderOverride } from \"./model-registry-types.ts\";\n\nconst GITHUB_COPILOT_API_VERSION_HEADER = \"X-GitHub-Api-Version\";\nconst GITHUB_COPILOT_API_VERSION = \"2026-06-01\";\n\nfunction hasHeader(headers: Record<string, string> | undefined, headerName: string): boolean {\n\tif (!headers) return false;\n\tconst normalizedHeaderName = headerName.toLowerCase();\n\treturn Object.keys(headers).some((key) => key.toLowerCase() === normalizedHeaderName);\n}\n\nexport function withGitHubCopilotApiVersionHeader(\n\tmodel: Model<Api>,\n\theaders: Record<string, string> | undefined,\n): Record<string, string> | undefined {\n\tif (model.provider !== \"github-copilot\" || hasHeader(headers, GITHUB_COPILOT_API_VERSION_HEADER)) {\n\t\treturn headers;\n\t}\n\treturn { ...(headers ?? {}), [GITHUB_COPILOT_API_VERSION_HEADER]: GITHUB_COPILOT_API_VERSION };\n}\n\nfunction withCopilotEnvironmentBaseUrl(model: Model<Api>): Model<Api> {\n\tif (model.provider !== \"github-copilot\") return model;\n\tconst resolvedBaseUrl = copilotApiBaseUrlFromToken(copilotTokenFromEnvironment());\n\tif (resolvedBaseUrl === DEFAULT_COPILOT_API_BASE_URL || resolvedBaseUrl === model.baseUrl) return model;\n\treturn { ...model, baseUrl: resolvedBaseUrl };\n}\n\nfunction withCopilotContextWindowOptions(model: Model<Api>): Model<Api> {\n\tif (model.provider !== \"github-copilot\") return model;\n\tconst context = getActiveCopilotModelCatalog().get(model.id);\n\tif (!context) return model;\n\tconst base = { ...model, contextWindow: context.contextWindow, maxInputTokens: context.maxInputTokens };\n\tif (context.contextWindowOptions && context.contextWindowOptions.length > 1) {\n\t\treturn withContextWindowOptions(base, context.contextWindowOptions);\n\t}\n\treturn base;\n}\n\nexport function mergeCompat(\n\tbaseCompat: Model<Api>[\"compat\"],\n\toverrideCompat: ModelOverride[\"compat\"] | Model<Api>[\"compat\"],\n): Model<Api>[\"compat\"] | undefined {\n\tif (!overrideCompat) return baseCompat;\n\n\tconst base = baseCompat as ProviderCompat | undefined;\n\tconst override = overrideCompat as ProviderCompat;\n\tconst merged = { ...base, ...override } as ProviderCompat;\n\n\tconst baseCompletions = base as OpenAICompletionsCompat | undefined;\n\tconst overrideCompletions = override as OpenAICompletionsCompat;\n\tconst mergedCompletions = merged as OpenAICompletionsCompat;\n\n\tif (baseCompletions?.openRouterRouting || overrideCompletions.openRouterRouting) {\n\t\tmergedCompletions.openRouterRouting = {\n\t\t\t...baseCompletions?.openRouterRouting,\n\t\t\t...overrideCompletions.openRouterRouting,\n\t\t};\n\t}\n\n\tif (baseCompletions?.vercelGatewayRouting || overrideCompletions.vercelGatewayRouting) {\n\t\tmergedCompletions.vercelGatewayRouting = {\n\t\t\t...baseCompletions?.vercelGatewayRouting,\n\t\t\t...overrideCompletions.vercelGatewayRouting,\n\t\t};\n\t}\n\n\tif (baseCompletions?.chatTemplateKwargs || overrideCompletions.chatTemplateKwargs) {\n\t\tmergedCompletions.chatTemplateKwargs = {\n\t\t\t...baseCompletions?.chatTemplateKwargs,\n\t\t\t...overrideCompletions.chatTemplateKwargs,\n\t\t};\n\t}\n\n\treturn merged as Model<Api>[\"compat\"];\n}\n\nexport function applyModelOverride(model: Model<Api>, override: ModelOverride): Model<Api> {\n\tconst result = { ...model };\n\n\tif (override.name !== undefined) result.name = override.name;\n\tif (override.reasoning !== undefined) result.reasoning = override.reasoning;\n\tif (override.thinkingLevelMap !== undefined) {\n\t\tresult.thinkingLevelMap = { ...model.thinkingLevelMap, ...override.thinkingLevelMap };\n\t}\n\tif (override.input !== undefined) result.input = override.input as (\"text\" | \"image\")[];\n\tif (override.contextWindow !== undefined) {\n\t\tresult.contextWindow = override.contextWindow;\n\t\tresult.defaultContextWindow = override.contextWindow;\n\t\tif (override.contextWindowOptions === undefined) {\n\t\t\tresult.contextWindowOptions = undefined;\n\t\t}\n\t}\n\tif (override.contextWindowOptions !== undefined) {\n\t\tresult.contextWindowOptions = normalizeContextWindowOptions(override.contextWindowOptions);\n\t}\n\tif (override.maxTokens !== undefined) result.maxTokens = override.maxTokens;\n\n\tif (override.cost) {\n\t\tresult.cost = {\n\t\t\tinput: override.cost.input ?? model.cost.input,\n\t\t\toutput: override.cost.output ?? model.cost.output,\n\t\t\tcacheRead: override.cost.cacheRead ?? model.cost.cacheRead,\n\t\t\tcacheWrite: override.cost.cacheWrite ?? model.cost.cacheWrite,\n\t\t};\n\t}\n\n\tresult.compat = mergeCompat(model.compat, override.compat);\n\treturn result;\n}\n\nexport function loadBuiltInModels(\n\toverrides: Map<string, ProviderOverride>,\n\tmodelOverrides: Map<string, Map<string, ModelOverride>>,\n): Model<Api>[] {\n\treturn getProviders().flatMap((provider) => {\n\t\tconst models = getModels(provider as KnownProvider) as Model<Api>[];\n\t\tconst providerOverride = overrides.get(provider);\n\t\tconst perModelOverrides = modelOverrides.get(provider);\n\n\t\treturn models.map((m) => {\n\t\t\tlet model = withCopilotEnvironmentBaseUrl(m);\n\n\t\t\tif (providerOverride) {\n\t\t\t\tmodel = {\n\t\t\t\t\t...model,\n\t\t\t\t\tbaseUrl: providerOverride.baseUrl ?? model.baseUrl,\n\t\t\t\t\tcompat: mergeCompat(model.compat, providerOverride.compat),\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tmodel = withCopilotContextWindowOptions(model);\n\t\t\tconst modelOverride = perModelOverrides?.get(m.id);\n\t\t\treturn modelOverride ? applyModelOverride(model, modelOverride) : model;\n\t\t});\n\t});\n}\n\nexport function mergeCustomModels(builtInModels: Model<Api>[], customModels: Model<Api>[]): Model<Api>[] {\n\tconst merged = [...builtInModels];\n\tfor (const customModel of customModels) {\n\t\tconst existingIndex = merged.findIndex((m) => m.provider === customModel.provider && m.id === customModel.id);\n\t\tif (existingIndex >= 0) {\n\t\t\tmerged[existingIndex] = customModel;\n\t\t} else {\n\t\t\tmerged.push(customModel);\n\t\t}\n\t}\n\treturn merged;\n}\n"]}