mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-19 13:23:50 +03:00
* fix(copilot): fallback to copilot-chat on 403 identity denial for standard provider * fix(copilot): document COPILOT_INTEGRATION_ID, extract identity fallback, add changelog Adds the missing COPILOT_INTEGRATION_ID entry to .env.example (fixes tests/unit/issue-7793-env-doc-sync-repro.test.ts), extracts the GitHub Copilot 403 identity fallback out of open-sse/executors/base.ts into its own module (open-sse/executors/copilotIdentityFallback.ts) to bring the file back under the frozen file-size ratchet, and adds a changelog.d/fixes fragment for the PR. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: tuandinh0801 <tuandinh0801@users.noreply.github.com>
110 lines
3.8 KiB
TypeScript
110 lines
3.8 KiB
TypeScript
import { HTTP_STATUS } from "../config/constants.ts";
|
|
import {
|
|
GITHUB_COPILOT_CLI_INTEGRATION_ID,
|
|
GITHUB_COPILOT_CHAT_INTEGRATION_ID,
|
|
resolveCopilotIntegrationIdOverride,
|
|
} from "../config/providerHeaderProfiles.ts";
|
|
import type { ExecutorLog } from "./base.ts";
|
|
|
|
export function readHeaderCaseInsensitive(
|
|
headers: Record<string, string> | null | undefined,
|
|
name: string
|
|
): string | null {
|
|
if (!headers) return null;
|
|
const target = name.toLowerCase();
|
|
const direct = headers[name] ?? headers[target];
|
|
if (typeof direct === "string") return direct;
|
|
for (const key in headers) {
|
|
if (key.toLowerCase() === target && typeof headers[key] === "string") {
|
|
return headers[key];
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export type CopilotIdentityFallbackArgs = {
|
|
response: Response;
|
|
url: string;
|
|
fetchOptions: RequestInit;
|
|
clientHeaders?: Record<string, string> | null;
|
|
fetchWithStartTimeout: (requestUrl: string, requestOptions: RequestInit) => Promise<Response>;
|
|
};
|
|
|
|
export type CopilotIdentityFallbackResult = {
|
|
response: Response;
|
|
finalHeaders: Record<string, string>;
|
|
};
|
|
|
|
/**
|
|
* GitHub Copilot 403 identity fallback: business/org accounts may reject the CLI
|
|
* identity (copilot-developer-cli) while allowing copilot-chat. Returns a bound
|
|
* per-`execute()`-call helper (provider + log fixed, retry state kept in the
|
|
* closure) so at most one identity retry happens across every fallback URL,
|
|
* gated strictly to standard github (not ghe-copilot), and only when identity
|
|
* was not explicitly pinned by the client or the operator.
|
|
*/
|
|
export function createCopilotIdentityFallback(provider: string, log?: ExecutorLog | null) {
|
|
let retried = false;
|
|
|
|
return async function applyCopilotIdentityFallback(
|
|
args: CopilotIdentityFallbackArgs
|
|
): Promise<CopilotIdentityFallbackResult> {
|
|
const { response, url, fetchOptions, clientHeaders, fetchWithStartTimeout } = args;
|
|
const finalHeaders = fetchOptions.headers as Record<string, string>;
|
|
|
|
if (
|
|
retried ||
|
|
provider !== "github" ||
|
|
response.status !== HTTP_STATUS.FORBIDDEN ||
|
|
resolveCopilotIntegrationIdOverride() ||
|
|
process.env.COPILOT_INTEGRATION_ID?.trim() ||
|
|
readHeaderCaseInsensitive(clientHeaders, "copilot-integration-id")?.trim()
|
|
) {
|
|
return { response, finalHeaders };
|
|
}
|
|
|
|
const currentIntegrationId = readHeaderCaseInsensitive(finalHeaders, "copilot-integration-id");
|
|
if (currentIntegrationId !== GITHUB_COPILOT_CLI_INTEGRATION_ID) {
|
|
return { response, finalHeaders };
|
|
}
|
|
|
|
const errText = await response
|
|
.clone()
|
|
.text()
|
|
.catch(() => "");
|
|
const isQuotaError = /quota|rate[_-]?limit|exceeded|insufficient_quota/i.test(errText);
|
|
const hasIdentityEvidence =
|
|
!isQuotaError &&
|
|
(/access denied/i.test(errText) ||
|
|
(/copilot/i.test(errText) && /403/.test(errText)) ||
|
|
/integration[_-]?id/i.test(errText) ||
|
|
/not (?:permitted|allowed|authorized)/i.test(errText));
|
|
|
|
if (!hasIdentityEvidence) {
|
|
return { response, finalHeaders };
|
|
}
|
|
|
|
await response.text().catch(() => "");
|
|
log?.warn?.(
|
|
"COPILOT_IDENTITY",
|
|
`Standard GitHub Copilot identity ${GITHUB_COPILOT_CLI_INTEGRATION_ID} denied (403) — retrying once with ${GITHUB_COPILOT_CHAT_INTEGRATION_ID}`
|
|
);
|
|
const retryHeaders: Record<string, string> = {
|
|
...finalHeaders,
|
|
"copilot-integration-id": GITHUB_COPILOT_CHAT_INTEGRATION_ID,
|
|
};
|
|
for (const key of Object.keys(retryHeaders)) {
|
|
if (key.toLowerCase() === "copilot-integration-id" && key !== "copilot-integration-id") {
|
|
delete retryHeaders[key];
|
|
}
|
|
}
|
|
retried = true;
|
|
const retryResponse = await fetchWithStartTimeout(url, {
|
|
...fetchOptions,
|
|
headers: retryHeaders,
|
|
});
|
|
|
|
return { response: retryResponse, finalHeaders: retryHeaders };
|
|
};
|
|
}
|