fix(github): use copilot token and materialize tls responses
This commit is contained in:
@@ -7,6 +7,18 @@ export class GithubExecutor extends BaseExecutor {
|
||||
super("github", PROVIDERS.github);
|
||||
}
|
||||
|
||||
getCopilotToken(credentials) {
|
||||
return credentials?.copilotToken || credentials?.providerSpecificData?.copilotToken || null;
|
||||
}
|
||||
|
||||
getCopilotTokenExpiresAt(credentials) {
|
||||
return (
|
||||
credentials?.copilotTokenExpiresAt ||
|
||||
credentials?.providerSpecificData?.copilotTokenExpiresAt ||
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
buildUrl(model, stream, urlIndex = 0) {
|
||||
const targetFormat = getModelTargetFormat("gh", model);
|
||||
if (targetFormat === "openai-responses") {
|
||||
@@ -73,7 +85,21 @@ export class GithubExecutor extends BaseExecutor {
|
||||
|
||||
async execute(input: ExecuteInput) {
|
||||
const result = await super.execute(input);
|
||||
if (!result || !result.response?.body) return result;
|
||||
if (!result || !result.response) return result;
|
||||
|
||||
if (!input.stream) {
|
||||
// wreq-js clone/text semantics consume the original response body. Materialize
|
||||
// non-streaming responses immediately so downstream code always sees a native
|
||||
// fetch Response with a readable body.
|
||||
const status = result.response.status;
|
||||
const statusText = result.response.statusText;
|
||||
const headers = new Headers(result.response.headers);
|
||||
const payload = await result.response.text();
|
||||
result.response = new Response(payload, { status, statusText, headers });
|
||||
return result;
|
||||
}
|
||||
|
||||
if (!result.response.body) return result;
|
||||
|
||||
const isStreaming = input.stream === true;
|
||||
const contentType = (result.response.headers.get("content-type") || "").toLowerCase();
|
||||
@@ -105,7 +131,7 @@ export class GithubExecutor extends BaseExecutor {
|
||||
}
|
||||
|
||||
buildHeaders(credentials, stream = true) {
|
||||
const token = credentials.copilotToken || credentials.accessToken;
|
||||
const token = this.getCopilotToken(credentials) || credentials.accessToken;
|
||||
return {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
@@ -213,11 +239,12 @@ export class GithubExecutor extends BaseExecutor {
|
||||
|
||||
needsRefresh(credentials) {
|
||||
// Always refresh if no copilotToken
|
||||
if (!credentials.copilotToken) return true;
|
||||
if (!this.getCopilotToken(credentials)) return true;
|
||||
|
||||
if (credentials.copilotTokenExpiresAt) {
|
||||
const copilotTokenExpiresAt = this.getCopilotTokenExpiresAt(credentials);
|
||||
if (copilotTokenExpiresAt) {
|
||||
// Handle both Unix timestamp (seconds) and ISO string
|
||||
let expiresAtMs = credentials.copilotTokenExpiresAt;
|
||||
let expiresAtMs = copilotTokenExpiresAt;
|
||||
if (typeof expiresAtMs === "number" && expiresAtMs < 1e12) {
|
||||
expiresAtMs = expiresAtMs * 1000; // Convert seconds to ms
|
||||
} else if (typeof expiresAtMs === "string") {
|
||||
|
||||
+18
-20
@@ -1010,17 +1010,16 @@ export async function handleComboChat({
|
||||
try {
|
||||
const cloned = result.clone();
|
||||
try {
|
||||
const errorBody = await cloned.json();
|
||||
errorText =
|
||||
errorBody?.error?.message || errorBody?.error || errorBody?.message || errorText;
|
||||
retryAfter = errorBody?.retryAfter || null;
|
||||
} catch {
|
||||
try {
|
||||
const text = await result.text();
|
||||
if (text) errorText = text.substring(0, 500);
|
||||
} catch {
|
||||
/* Body consumed */
|
||||
const text = await cloned.text();
|
||||
if (text) {
|
||||
errorText = text.substring(0, 500);
|
||||
const errorBody = JSON.parse(text);
|
||||
errorText =
|
||||
errorBody?.error?.message || errorBody?.error || errorBody?.message || errorText;
|
||||
retryAfter = errorBody?.retryAfter || null;
|
||||
}
|
||||
} catch {
|
||||
/* Clone parse failed */
|
||||
}
|
||||
} catch {
|
||||
/* Clone failed */
|
||||
@@ -1293,17 +1292,16 @@ async function handleRoundRobinCombo({
|
||||
try {
|
||||
const cloned = result.clone();
|
||||
try {
|
||||
const errorBody = await cloned.json();
|
||||
errorText =
|
||||
errorBody?.error?.message || errorBody?.error || errorBody?.message || errorText;
|
||||
retryAfter = errorBody?.retryAfter || null;
|
||||
} catch {
|
||||
try {
|
||||
const text = await result.text();
|
||||
if (text) errorText = text.substring(0, 500);
|
||||
} catch {
|
||||
/* Body consumed */
|
||||
const text = await cloned.text();
|
||||
if (text) {
|
||||
errorText = text.substring(0, 500);
|
||||
const errorBody = JSON.parse(text);
|
||||
errorText =
|
||||
errorBody?.error?.message || errorBody?.error || errorBody?.message || errorText;
|
||||
retryAfter = errorBody?.retryAfter || null;
|
||||
}
|
||||
} catch {
|
||||
/* Clone parse failed */
|
||||
}
|
||||
} catch {
|
||||
/* Clone failed */
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const { GithubExecutor } = await import("../../open-sse/executors/github.ts");
|
||||
const { BaseExecutor } = await import("../../open-sse/executors/base.ts");
|
||||
|
||||
@@ -16,6 +15,16 @@ function streamFromChunks(chunks) {
|
||||
});
|
||||
}
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
test.afterEach(async () => {
|
||||
globalThis.fetch = originalFetch;
|
||||
});
|
||||
|
||||
test.after(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
});
|
||||
|
||||
test("T27: Claude + response_format=json_object injects system instruction and strips response_format field", () => {
|
||||
const executor = new GithubExecutor();
|
||||
const request = {
|
||||
@@ -109,3 +118,103 @@ test("T27: streaming error responses keep their original body readable", async (
|
||||
BaseExecutor.prototype.execute = originalExecute;
|
||||
}
|
||||
});
|
||||
|
||||
test("T27: requests use copilotToken from providerSpecificData when available", async () => {
|
||||
globalThis.fetch = async (_url, init = {}) => {
|
||||
assert.equal(init.headers.Authorization, "Bearer copilot_test");
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
choices: [
|
||||
{ index: 0, message: { role: "assistant", content: "OK" }, finish_reason: "stop" },
|
||||
],
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const executor = new GithubExecutor();
|
||||
const result = await executor.execute({
|
||||
model: "gemini-3.1-pro-preview",
|
||||
body: { messages: [{ role: "user", content: "Ping" }], stream: false },
|
||||
stream: false,
|
||||
credentials: {
|
||||
accessToken: "ghu_test",
|
||||
providerSpecificData: {
|
||||
copilotToken: "copilot_test",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result.response.status, 200);
|
||||
assert.match(await result.response.text(), /OK/);
|
||||
});
|
||||
|
||||
test("T27: non-stream execute materializes provider responses before returning", async () => {
|
||||
const executor = new GithubExecutor();
|
||||
const originalExecute = BaseExecutor.prototype.execute;
|
||||
|
||||
class WeirdResponse {
|
||||
constructor(body, init = {}) {
|
||||
this._body = body;
|
||||
this.status = init.status || 200;
|
||||
this.statusText = init.statusText || "OK";
|
||||
this.headers = new Headers(init.headers || {});
|
||||
this.bodyUsed = false;
|
||||
this.body = {};
|
||||
}
|
||||
|
||||
async text() {
|
||||
if (this.bodyUsed) {
|
||||
throw new TypeError("Response body is already used");
|
||||
}
|
||||
this.bodyUsed = true;
|
||||
return this._body;
|
||||
}
|
||||
}
|
||||
|
||||
BaseExecutor.prototype.execute = async () => ({
|
||||
response: new WeirdResponse(JSON.stringify({ ok: true }), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
}),
|
||||
url: "https://api.githubcopilot.com/chat/completions",
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await executor.execute({
|
||||
model: "gemini-3.1-pro-preview",
|
||||
body: { messages: [{ role: "user", content: "Ping" }], stream: false },
|
||||
stream: false,
|
||||
credentials: {
|
||||
accessToken: "ghu_test",
|
||||
providerSpecificData: {
|
||||
copilotToken: "copilot_test",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result.response.constructor.name, "Response");
|
||||
assert.equal(await result.response.text(), JSON.stringify({ ok: true }));
|
||||
} finally {
|
||||
BaseExecutor.prototype.execute = originalExecute;
|
||||
}
|
||||
});
|
||||
|
||||
test("T27: needsRefresh respects providerSpecificData copilot token metadata", () => {
|
||||
const executor = new GithubExecutor();
|
||||
const expiresAt = Math.floor(Date.now() / 1000) + 3600;
|
||||
|
||||
assert.equal(
|
||||
executor.needsRefresh({
|
||||
accessToken: "ghu_test",
|
||||
providerSpecificData: {
|
||||
copilotToken: "copilot_test",
|
||||
copilotTokenExpiresAt: expiresAt,
|
||||
},
|
||||
}),
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user