fix(catalog): bound Copilot endpoint probe with discovery timeout
Threaded the shared 10s discovery AbortSignal into the copilot_internal/user probe so a stalled endpoint falls back to the personal host instead of hanging startup or refresh. Fixes #8507
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { USER_AGENT } from "@oh-my-pi/pi-utils";
|
||||
import * as logger from "@oh-my-pi/pi-utils/logger";
|
||||
import {
|
||||
DEFAULT_OPENAI_COMPATIBLE_DISCOVERY_TIMEOUT_MS,
|
||||
fetchOpenAICompatibleModels,
|
||||
type OpenAICompatibleModelMapperContext,
|
||||
type OpenAICompatibleModelRecord,
|
||||
@@ -5204,7 +5205,9 @@ export function githubCopilotModelManagerOptions(config?: GithubCopilotModelMana
|
||||
fetchDynamicModels: async () => {
|
||||
const fetchImpl = discoveryFetch(config?.fetch);
|
||||
const requestBaseUrl = isPersonalGitHubCopilotBaseUrl(baseUrl)
|
||||
? ((await discoverGitHubCopilotApiEndpoint(apiKey, fetchImpl)) ?? baseUrl)
|
||||
? ((await withCatalogDiscoveryTimeout(DEFAULT_OPENAI_COMPATIBLE_DISCOVERY_TIMEOUT_MS, signal =>
|
||||
discoverGitHubCopilotApiEndpoint(apiKey, fetchImpl, signal),
|
||||
)) ?? baseUrl)
|
||||
: baseUrl;
|
||||
const longContextVariants: ModelSpec<Api>[] = [];
|
||||
const models = await fetchOpenAICompatibleModels<Api>({
|
||||
|
||||
@@ -77,11 +77,14 @@ export function normalizeGitHubCopilotApiEndpoint(input: string | undefined): st
|
||||
}
|
||||
/**
|
||||
* Resolve the plan-specific Copilot API endpoint advertised for a GitHub token.
|
||||
* Login and raw environment-token discovery share this best-effort probe.
|
||||
* Login and raw environment-token discovery share this best-effort probe. Pass
|
||||
* a `signal` to bound it against the same discovery deadline as `/models`; a
|
||||
* stalled probe otherwise blocks discovery indefinitely.
|
||||
*/
|
||||
export async function discoverGitHubCopilotApiEndpoint(
|
||||
token: string,
|
||||
fetchImpl: FetchImpl,
|
||||
signal?: AbortSignal,
|
||||
): Promise<string | undefined> {
|
||||
try {
|
||||
const response = await fetchImpl("https://api.github.com/copilot_internal/user", {
|
||||
@@ -90,6 +93,7 @@ export async function discoverGitHubCopilotApiEndpoint(
|
||||
Authorization: `token ${token}`,
|
||||
...OPENCODE_HEADERS,
|
||||
},
|
||||
signal,
|
||||
});
|
||||
if (!response.ok) return undefined;
|
||||
const data: unknown = await response.json();
|
||||
|
||||
@@ -44,6 +44,9 @@ async function discoverCopilotModels(
|
||||
const url = typeof input === "string" ? input : input.toString();
|
||||
if (url === "https://api.github.com/copilot_internal/user") {
|
||||
expect(getHeaderValue(init?.headers, "Authorization")).toBe(`token ${expectedAuthorizationToken}`);
|
||||
// The probe must be bounded by the shared discovery deadline so a
|
||||
// stalled endpoint cannot hang discovery (PR #8510 review).
|
||||
expect(init?.signal).toBeInstanceOf(AbortSignal);
|
||||
return Response.json({ endpoints: { api: expectedBaseUrl } });
|
||||
}
|
||||
expect(url).toBe(`${expectedBaseUrl}/models`);
|
||||
@@ -86,6 +89,21 @@ describe("github copilot model limits mapping", () => {
|
||||
);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
it("falls back to the personal endpoint when the raw-token probe fails", async () => {
|
||||
const token = "ghu_valid_business_token";
|
||||
const fetchMock = vi.fn(async (input: string | URL | Request, init?: RequestInit) => {
|
||||
const url = typeof input === "string" ? input : input.toString();
|
||||
if (url === "https://api.github.com/copilot_internal/user") {
|
||||
expect(init?.signal).toBeInstanceOf(AbortSignal);
|
||||
throw new DOMException("The operation timed out.", "TimeoutError");
|
||||
}
|
||||
expect(url).toBe("https://api.githubcopilot.com/models");
|
||||
return Response.json({ data: [] });
|
||||
});
|
||||
const models = await githubCopilotModelManagerOptions({ apiKey: token, fetch: fetchMock }).fetchDynamicModels?.();
|
||||
expect(models).toEqual([]);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("unwraps structured OAuth keys for discovery and routes enterprise discovery to the enterprise host", async () => {
|
||||
const structuredApiKey = JSON.stringify({
|
||||
|
||||
Reference in New Issue
Block a user