mirror of https://github.com/openclaw/openclaw.git
fix(providers): classify copilot native endpoints (#59644)
* fix(providers): classify copilot native endpoints * fix(changelog): add copilot endpoint note * fix(providers): handle copilot proxy hints
This commit is contained in:
parent
71d49012fc
commit
5abd5d889f
|
|
@ -51,6 +51,7 @@ Docs: https://docs.openclaw.ai
|
|||
- MS Teams/logging: format non-`Error` failures with the shared unknown-error helper so logs stop collapsing caught SDK or Axios objects into `[object Object]`. (#59321) Thanks @bradgroux.
|
||||
- Slack/thread context: filter thread starter and history by the effective conversation allowlist without dropping valid open-room, DM, or group DM context. (#58380) Thanks @jacobtomlinson.
|
||||
- ACP/gateway reconnects: reject stale pre-ack ACP prompts after reconnect grace expiry so callers fail cleanly instead of hanging indefinitely when the gateway never confirms the run.
|
||||
- Providers/Copilot: classify native GitHub Copilot API hosts in the shared provider endpoint resolver and harden token-derived proxy endpoint parsing so Copilot base URL routing stays centralized and fails closed on malformed hints. Thanks @vincentkoc.
|
||||
|
||||
## 2026.4.1-beta.1
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,31 @@
|
|||
import { describe, expect, it, vi } from "vitest";
|
||||
import { resolveCopilotApiToken } from "./github-copilot-token.js";
|
||||
import {
|
||||
deriveCopilotApiBaseUrlFromToken,
|
||||
resolveCopilotApiToken,
|
||||
} from "./github-copilot-token.js";
|
||||
|
||||
describe("resolveCopilotApiToken", () => {
|
||||
it("derives native Copilot base URLs from Copilot proxy hints", () => {
|
||||
expect(
|
||||
deriveCopilotApiBaseUrlFromToken(
|
||||
"copilot-token;proxy-ep=https://proxy.individual.githubcopilot.com;",
|
||||
),
|
||||
).toBe("https://api.individual.githubcopilot.com");
|
||||
expect(deriveCopilotApiBaseUrlFromToken("copilot-token;proxy-ep=proxy.example.com;")).toBe(
|
||||
"https://api.example.com",
|
||||
);
|
||||
expect(deriveCopilotApiBaseUrlFromToken("copilot-token;proxy-ep=proxy.example.com:8443;")).toBe(
|
||||
"https://api.example.com",
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects malformed or non-http proxy hints", () => {
|
||||
expect(
|
||||
deriveCopilotApiBaseUrlFromToken("copilot-token;proxy-ep=javascript:alert(1);"),
|
||||
).toBeNull();
|
||||
expect(deriveCopilotApiBaseUrlFromToken("copilot-token;proxy-ep=://bad;")).toBeNull();
|
||||
});
|
||||
|
||||
it("treats 11-digit expires_at values as seconds epochs", async () => {
|
||||
const fetchImpl = vi.fn(async () => ({
|
||||
ok: true,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import path from "node:path";
|
||||
import { resolveStateDir } from "../config/paths.js";
|
||||
import { loadJsonFile, saveJsonFile } from "../infra/json-file.js";
|
||||
import { resolveProviderEndpoint } from "./provider-attribution.js";
|
||||
|
||||
const COPILOT_TOKEN_URL = "https://api.github.com/copilot_internal/v2/token";
|
||||
|
||||
|
|
@ -55,6 +56,24 @@ function parseCopilotTokenResponse(value: unknown): {
|
|||
|
||||
export const DEFAULT_COPILOT_API_BASE_URL = "https://api.individual.githubcopilot.com";
|
||||
|
||||
function resolveCopilotProxyHost(proxyEp: string): string | null {
|
||||
const trimmed = proxyEp.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const urlText = /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
|
||||
try {
|
||||
const url = new URL(urlText);
|
||||
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
||||
return null;
|
||||
}
|
||||
return url.hostname.toLowerCase();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function deriveCopilotApiBaseUrlFromToken(token: string): string | null {
|
||||
const trimmed = token.trim();
|
||||
if (!trimmed) {
|
||||
|
|
@ -71,12 +90,14 @@ export function deriveCopilotApiBaseUrlFromToken(token: string): string | null {
|
|||
|
||||
// pi-ai expects converting proxy.* -> api.*
|
||||
// (see upstream getGitHubCopilotBaseUrl).
|
||||
const host = proxyEp.replace(/^https?:\/\//, "").replace(/^proxy\./i, "api.");
|
||||
if (!host) {
|
||||
const proxyHost = resolveCopilotProxyHost(proxyEp);
|
||||
if (!proxyHost) {
|
||||
return null;
|
||||
}
|
||||
const host = proxyHost.replace(/^proxy\./i, "api.");
|
||||
|
||||
return `https://${host}`;
|
||||
const baseUrl = `https://${host}`;
|
||||
return resolveProviderEndpoint(baseUrl).endpointClass === "invalid" ? null : baseUrl;
|
||||
}
|
||||
|
||||
export async function resolveCopilotApiToken(params: {
|
||||
|
|
|
|||
|
|
@ -340,6 +340,23 @@ describe("provider attribution", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("classifies native GitHub Copilot endpoints separately from custom hosts", () => {
|
||||
expect(resolveProviderEndpoint("https://api.individual.githubcopilot.com")).toMatchObject({
|
||||
endpointClass: "github-copilot-native",
|
||||
hostname: "api.individual.githubcopilot.com",
|
||||
});
|
||||
|
||||
expect(resolveProviderEndpoint("https://api.enterprise.githubcopilot.com")).toMatchObject({
|
||||
endpointClass: "github-copilot-native",
|
||||
hostname: "api.enterprise.githubcopilot.com",
|
||||
});
|
||||
|
||||
expect(resolveProviderEndpoint("https://api.githubcopilot.example.com")).toMatchObject({
|
||||
endpointClass: "custom",
|
||||
hostname: "api.githubcopilot.example.com",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not classify malformed or embedded Google host strings as native endpoints", () => {
|
||||
expect(resolveProviderEndpoint("proxy/generativelanguage.googleapis.com")).toMatchObject({
|
||||
endpointClass: "custom",
|
||||
|
|
@ -516,4 +533,20 @@ describe("provider attribution", () => {
|
|||
compatibilityFamily: "moonshot",
|
||||
});
|
||||
});
|
||||
|
||||
it("treats native GitHub Copilot base URLs as known native endpoints", () => {
|
||||
expect(
|
||||
resolveProviderRequestCapabilities({
|
||||
provider: "github-copilot",
|
||||
api: "openai-responses",
|
||||
baseUrl: "https://api.individual.githubcopilot.com",
|
||||
capability: "llm",
|
||||
transport: "http",
|
||||
}),
|
||||
).toMatchObject({
|
||||
endpointClass: "github-copilot-native",
|
||||
knownProviderFamily: "github-copilot",
|
||||
isKnownNativeEndpoint: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ export type ProviderRequestCapability = "llm" | "audio" | "image" | "video" | "o
|
|||
export type ProviderEndpointClass =
|
||||
| "default"
|
||||
| "anthropic-public"
|
||||
| "github-copilot-native"
|
||||
| "moonshot-native"
|
||||
| "modelstudio-native"
|
||||
| "openai-public"
|
||||
|
|
@ -201,6 +202,9 @@ export function resolveProviderEndpoint(
|
|||
if (host === "api.anthropic.com") {
|
||||
return { endpointClass: "anthropic-public", hostname: host };
|
||||
}
|
||||
if (host.endsWith(".githubcopilot.com")) {
|
||||
return { endpointClass: "github-copilot-native", hostname: host };
|
||||
}
|
||||
if (host === "chatgpt.com") {
|
||||
return { endpointClass: "openai-codex", hostname: host };
|
||||
}
|
||||
|
|
@ -494,6 +498,7 @@ export function resolveProviderRequestCapabilities(
|
|||
const endpointClass = policy.endpointClass;
|
||||
const isKnownNativeEndpoint =
|
||||
endpointClass === "anthropic-public" ||
|
||||
endpointClass === "github-copilot-native" ||
|
||||
endpointClass === "moonshot-native" ||
|
||||
endpointClass === "modelstudio-native" ||
|
||||
endpointClass === "openai-public" ||
|
||||
|
|
|
|||
Loading…
Reference in New Issue