mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-20 22:02:19 +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>
238 lines
8.5 KiB
TypeScript
238 lines
8.5 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { GheCopilotExecutor } from "../../open-sse/executors/ghe-copilot.ts";
|
|
import { gheCopilotProvider } from "../../open-sse/config/providers/registry/ghe-copilot/index.ts";
|
|
import { GHE_COPILOT_TARGET } from "../../src/mitm/targets/ghe-copilot.ts";
|
|
import type { ProviderCredentials } from "../../open-sse/executors/base.ts";
|
|
|
|
test("GHE Copilot registry exposes Claude Opus 5", () => {
|
|
const opus5 = gheCopilotProvider.models.find((model) => model.id === "claude-opus-5");
|
|
|
|
assert.deepStrictEqual(opus5, {
|
|
id: "claude-opus-5",
|
|
name: "Claude Opus 5",
|
|
contextLength: 1000000,
|
|
maxOutputTokens: 64000,
|
|
unsupportedParams: ["temperature", "top_p", "top_k"],
|
|
});
|
|
});
|
|
|
|
test("GHE_COPILOT_TARGET has correct id and patterns", () => {
|
|
assert.strictEqual(GHE_COPILOT_TARGET.id, "ghe-copilot");
|
|
assert.deepStrictEqual(GHE_COPILOT_TARGET.endpointPatterns, [
|
|
"/chat/completions",
|
|
"/v1/chat/completions",
|
|
"/responses",
|
|
]);
|
|
});
|
|
|
|
test("buildUrl uses gheUrl for chat/completions with credentials", () => {
|
|
const executor = new GheCopilotExecutor({
|
|
gheUrl: "https://ghe.company.com",
|
|
clientId: "test-client",
|
|
clientSecret: "test-secret",
|
|
});
|
|
const credentials: ProviderCredentials = {
|
|
providerSpecificData: { gheUrl: "https://ghe.company.com" },
|
|
};
|
|
const url = executor.buildUrl("gpt-4o", true, 0, credentials);
|
|
assert.strictEqual(url, "https://ghe.company.com/chat/completions");
|
|
});
|
|
|
|
test("buildUrl uses gheUrl for responses endpoint with codex model", () => {
|
|
const executor = new GheCopilotExecutor({
|
|
gheUrl: "https://ghe.company.com",
|
|
clientId: "test-client",
|
|
clientSecret: "test-secret",
|
|
});
|
|
const credentials: ProviderCredentials = {
|
|
providerSpecificData: { gheUrl: "https://ghe.company.com" },
|
|
};
|
|
const url = executor.buildUrl("gpt-4o-codex", true, 0, credentials);
|
|
assert.strictEqual(url, "https://ghe.company.com/responses");
|
|
});
|
|
|
|
test("buildUrl uses responses endpoint for gpt-5.4-mini and gpt-5.6-sol", () => {
|
|
const executor = new GheCopilotExecutor({
|
|
gheUrl: "https://ghe.company.com",
|
|
clientId: "test-client",
|
|
clientSecret: "test-secret",
|
|
});
|
|
const credentials: ProviderCredentials = {
|
|
providerSpecificData: { gheUrl: "https://ghe.company.com" },
|
|
};
|
|
assert.strictEqual(
|
|
executor.buildUrl("gpt-5.4-mini", true, 0, credentials),
|
|
"https://ghe.company.com/responses"
|
|
);
|
|
assert.strictEqual(
|
|
executor.buildUrl("ghe-copilot/gpt-5.6-sol", true, 0, credentials),
|
|
"https://ghe.company.com/responses"
|
|
);
|
|
});
|
|
|
|
test("buildUrl routes Claude to the native /v1/messages shim (not chat/completions)", () => {
|
|
const executor = new GheCopilotExecutor({
|
|
gheUrl: "https://ghe.company.com",
|
|
clientId: "test-client",
|
|
clientSecret: "test-secret",
|
|
});
|
|
const credentials: ProviderCredentials = {
|
|
providerSpecificData: { gheUrl: "https://ghe.company.com" },
|
|
};
|
|
// Claude must ALWAYS use the Anthropic-native shim (prompt-cache token counts +
|
|
// lossless tool_use/tool_result/thinking blocks), same as github.com Copilot.
|
|
assert.strictEqual(
|
|
executor.buildUrl("claude-opus-5", true, 0, credentials),
|
|
"https://ghe.company.com/v1/messages"
|
|
);
|
|
});
|
|
|
|
test("buildUrl uses chat/completions endpoint for gemini models", () => {
|
|
const executor = new GheCopilotExecutor({
|
|
gheUrl: "https://ghe.company.com",
|
|
clientId: "test-client",
|
|
clientSecret: "test-secret",
|
|
});
|
|
const credentials: ProviderCredentials = {
|
|
providerSpecificData: { gheUrl: "https://ghe.company.com" },
|
|
};
|
|
// Gemini has no native shim on Copilot — it stays on /chat/completions.
|
|
assert.strictEqual(
|
|
executor.buildUrl("gemini-3.7-flash", true, 0, credentials),
|
|
"https://ghe.company.com/chat/completions"
|
|
);
|
|
});
|
|
|
|
test("buildUrl handles gheUrl with trailing slash", () => {
|
|
const exec = new GheCopilotExecutor({
|
|
gheUrl: "https://ghe.company.com/",
|
|
clientId: "test",
|
|
clientSecret: "test",
|
|
});
|
|
const credentials: ProviderCredentials = {
|
|
providerSpecificData: { gheUrl: "https://ghe.company.com/" },
|
|
};
|
|
const url = exec.buildUrl("gpt-4o", true, 0, credentials);
|
|
assert.strictEqual(url, "https://ghe.company.com/chat/completions");
|
|
});
|
|
|
|
test("buildUrl handles gheUrl already containing /chat/completions", () => {
|
|
const executor = new GheCopilotExecutor({
|
|
gheUrl: "https://ghe.company.com",
|
|
clientId: "test-client",
|
|
clientSecret: "test-secret",
|
|
});
|
|
const credentials: ProviderCredentials = {
|
|
providerSpecificData: { gheUrl: "https://ghe.company.com/chat/completions" },
|
|
};
|
|
const url = executor.buildUrl("gpt-4o", true, 0, credentials);
|
|
assert.strictEqual(url, "https://ghe.company.com/chat/completions");
|
|
});
|
|
|
|
test("buildUrl throws without gheUrl in credentials", () => {
|
|
const executor = new GheCopilotExecutor({
|
|
gheUrl: "https://ghe.company.com",
|
|
clientId: "test-client",
|
|
clientSecret: "test-secret",
|
|
});
|
|
const credentials: ProviderCredentials = { providerSpecificData: {} };
|
|
assert.throws(() => executor.buildUrl("gpt-4o", true, 0, credentials), {
|
|
message: "GHE Copilot executor requires gheUrl in providerSpecificData",
|
|
});
|
|
});
|
|
|
|
test("refreshCopilotToken delegates to GHE token endpoint", async () => {
|
|
const executor = new GheCopilotExecutor({
|
|
gheUrl: "https://ghe.company.com",
|
|
clientId: "test-client",
|
|
clientSecret: "test-secret",
|
|
});
|
|
const credentials: ProviderCredentials = {
|
|
providerSpecificData: { gheUrl: "https://ghe.company.com" },
|
|
copilotToken: "test-token",
|
|
};
|
|
|
|
const result = await executor.refreshCopilotToken("github-access-token", undefined, credentials);
|
|
assert.strictEqual(result, null);
|
|
});
|
|
|
|
test("refreshGitHubToken delegates to GHE OAuth endpoint", async () => {
|
|
const executor = new GheCopilotExecutor({
|
|
gheUrl: "https://ghe.company.com",
|
|
clientId: "test-client",
|
|
clientSecret: "test-secret",
|
|
});
|
|
const credentials: ProviderCredentials = {
|
|
providerSpecificData: { gheUrl: "https://ghe.company.com" },
|
|
};
|
|
|
|
const result = await executor.refreshGitHubToken("refresh-token", undefined, credentials);
|
|
assert.strictEqual(result, null);
|
|
});
|
|
|
|
test("executor extends GithubExecutor", () => {
|
|
const executor = new GheCopilotExecutor({
|
|
gheUrl: "https://ghe.company.com",
|
|
clientId: "test-client",
|
|
clientSecret: "test-secret",
|
|
});
|
|
assert.strictEqual(executor.constructor.name, "GheCopilotExecutor");
|
|
assert.strictEqual(executor.getProvider(), "ghe-copilot");
|
|
assert.strictEqual(executor.config.baseUrl, "https://api.githubcopilot.com/chat/completions");
|
|
});
|
|
|
|
test("isValidGheUrl accepts https enterprise hosts and rejects malformed or non-https input", async () => {
|
|
const { isValidGheUrl } = await import("../../src/shared/validation/providerSpecificData.ts");
|
|
assert.equal(isValidGheUrl("https://github.mycorp.example"), true);
|
|
assert.equal(isValidGheUrl("https://10.0.0.5"), true); // on-prem GHE on private IP is the primary use case
|
|
assert.equal(isValidGheUrl("http://github.mycorp.example"), false);
|
|
assert.equal(isValidGheUrl("javascript:alert(1)"), false);
|
|
assert.equal(isValidGheUrl("not a url"), false);
|
|
});
|
|
|
|
test("GheCopilotExecutor.execute does not trigger identity fallback on 403", async () => {
|
|
const executor = new GheCopilotExecutor({
|
|
gheUrl: "https://ghe.company.com",
|
|
clientId: "test-client",
|
|
clientSecret: "test-secret",
|
|
});
|
|
const originalFetch = globalThis.fetch;
|
|
let callCount = 0;
|
|
const seenIntegrationIds: string[] = [];
|
|
|
|
globalThis.fetch = async (_url, init: RequestInit = {}) => {
|
|
callCount++;
|
|
const headers = init.headers as Record<string, string>;
|
|
seenIntegrationIds.push(headers["copilot-integration-id"]);
|
|
return new Response(
|
|
JSON.stringify({ message: "Access denied: Enterprise Copilot 403 Forbidden" }),
|
|
{ status: 403, headers: { "Content-Type": "application/json" } }
|
|
);
|
|
};
|
|
|
|
try {
|
|
const credentials: ProviderCredentials = {
|
|
accessToken: "ghe-token",
|
|
providerSpecificData: {
|
|
gheUrl: "https://ghe.company.com",
|
|
copilotToken: "copilot-token",
|
|
},
|
|
};
|
|
|
|
const result = await executor.execute({
|
|
model: "gpt-4o",
|
|
body: { messages: [{ role: "user", content: "hi" }] },
|
|
stream: false,
|
|
credentials,
|
|
});
|
|
|
|
assert.equal(callCount, 1, "GHE Copilot must never retry on 403");
|
|
assert.deepEqual(seenIntegrationIds, ["copilot-developer-cli"]);
|
|
const res = result as { response: Response };
|
|
assert.equal(res.response.status, 403);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|