mirror of
https://github.com/earendil-works/pi.git
synced 2026-10-02 00:35:27 +08:00
@@ -108,6 +108,7 @@
|
||||
- Fixed cancellation while waiting for file-backed credential or model-catalog locks, preventing cancelled mutations from running or committing later.
|
||||
- Fixed concurrent in-memory credential mutations losing unrelated provider updates by serializing their read-modify-write sections.
|
||||
- Updated `undici` to 8.9.0 and the packaged `brace-expansion` to 5.0.9 to address GHSA-8xcm-r25x-g524, GHSA-4cwx-7wf7-3272, GHSA-m8rv-5g2x-5cg5, GHSA-jr45-8vmc-qm54, GHSA-v3r7-h72x-cjcm, and GHSA-rgw5-rvv9-x895.
|
||||
- Fixed GitHub Copilot compaction and branch summaries using the Individual endpoint instead of the credential-resolved Business or Enterprise endpoint ([#6768](https://github.com/earendil-works/pi/issues/6768)).
|
||||
|
||||
## [0.83.0] - 2026-07-29
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import type {
|
||||
} from "@earendil-works/pi-agent-core";
|
||||
import { contentText } from "@earendil-works/pi-ai";
|
||||
import type {
|
||||
Api,
|
||||
AssistantMessage,
|
||||
AuthResult,
|
||||
ImageContent,
|
||||
@@ -406,7 +407,10 @@ export class AgentSession {
|
||||
return this._modelRuntime;
|
||||
}
|
||||
|
||||
private async _getRequiredRequestAuth(model: Model<any>): Promise<{
|
||||
private async _getRequiredRequestAuth<TApi extends Api>(
|
||||
model: Model<TApi>,
|
||||
): Promise<{
|
||||
model: Model<TApi>;
|
||||
apiKey?: string;
|
||||
headers?: Record<string, string>;
|
||||
env?: Record<string, string>;
|
||||
@@ -422,7 +426,9 @@ export class AgentSession {
|
||||
throw error;
|
||||
}
|
||||
if (result && (result.auth.apiKey || result.auth.headers)) {
|
||||
const requestModel = result.auth.baseUrl ? { ...model, baseUrl: result.auth.baseUrl } : model;
|
||||
return {
|
||||
model: requestModel,
|
||||
apiKey: result.auth.apiKey,
|
||||
headers: withoutDeletedHeaders(result.auth.headers),
|
||||
env: result.env,
|
||||
@@ -440,7 +446,10 @@ export class AgentSession {
|
||||
throw new Error(formatNoApiKeyFoundMessage(model.provider));
|
||||
}
|
||||
|
||||
private async _getSummarizationRequestAuth(model: Model<any>): Promise<{
|
||||
private async _getSummarizationRequestAuth<TApi extends Api>(
|
||||
model: Model<TApi>,
|
||||
): Promise<{
|
||||
model: Model<TApi>;
|
||||
apiKey?: string;
|
||||
headers?: Record<string, string>;
|
||||
env?: Record<string, string>;
|
||||
@@ -451,11 +460,16 @@ export class AgentSession {
|
||||
|
||||
try {
|
||||
const result = await this._modelRuntime.getAuth(model);
|
||||
return result
|
||||
? { apiKey: result.auth.apiKey, headers: withoutDeletedHeaders(result.auth.headers), env: result.env }
|
||||
: {};
|
||||
if (!result) return { model };
|
||||
const requestModel = result.auth.baseUrl ? { ...model, baseUrl: result.auth.baseUrl } : model;
|
||||
return {
|
||||
model: requestModel,
|
||||
apiKey: result.auth.apiKey,
|
||||
headers: withoutDeletedHeaders(result.auth.headers),
|
||||
env: result.env,
|
||||
};
|
||||
} catch {
|
||||
return {};
|
||||
return { model };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1788,7 +1802,7 @@ export class AgentSession {
|
||||
throw new Error(formatNoModelSelectedMessage());
|
||||
}
|
||||
|
||||
const { apiKey, headers, env } = await this._getSummarizationRequestAuth(this.model);
|
||||
const { model: requestModel, apiKey, headers, env } = await this._getSummarizationRequestAuth(this.model);
|
||||
|
||||
const pathEntries = this.sessionManager.getBranch();
|
||||
const settings = this.settingsManager.getCompactionSettings();
|
||||
@@ -1844,7 +1858,7 @@ export class AgentSession {
|
||||
// Generate compaction result
|
||||
const result = await compact(
|
||||
preparation,
|
||||
this.model,
|
||||
requestModel,
|
||||
apiKey,
|
||||
headers,
|
||||
customInstructions,
|
||||
@@ -2052,14 +2066,7 @@ export class AgentSession {
|
||||
return false;
|
||||
}
|
||||
|
||||
let apiKey: string | undefined;
|
||||
let headers: Record<string, string> | undefined;
|
||||
let env: Record<string, string> | undefined;
|
||||
if (this.agent.streamFunction === streamSimple) {
|
||||
({ apiKey, headers, env } = await this._getRequiredRequestAuth(this.model));
|
||||
} else {
|
||||
({ apiKey, headers, env } = await this._getSummarizationRequestAuth(this.model));
|
||||
}
|
||||
const { model: requestModel, apiKey, headers, env } = await this._getSummarizationRequestAuth(this.model);
|
||||
|
||||
const pathEntries = this.sessionManager.getBranch();
|
||||
|
||||
@@ -2120,7 +2127,7 @@ export class AgentSession {
|
||||
// Generate compaction result
|
||||
const compactResult = await compact(
|
||||
preparation,
|
||||
this.model,
|
||||
requestModel,
|
||||
apiKey,
|
||||
headers,
|
||||
undefined,
|
||||
@@ -2985,10 +2992,10 @@ export class AgentSession {
|
||||
let summaryUsage: Usage | undefined;
|
||||
if (options.summarize && entriesToSummarize.length > 0 && !extensionSummary) {
|
||||
const model = this.model!;
|
||||
const { apiKey, headers, env } = await this._getSummarizationRequestAuth(model);
|
||||
const { model: requestModel, apiKey, headers, env } = await this._getSummarizationRequestAuth(model);
|
||||
const branchSummarySettings = this.settingsManager.getBranchSummarySettings();
|
||||
const result = await generateBranchSummary(entriesToSummarize, {
|
||||
model,
|
||||
model: requestModel,
|
||||
apiKey,
|
||||
headers,
|
||||
env,
|
||||
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
import {
|
||||
type AssistantMessage,
|
||||
createAssistantMessageEventStream,
|
||||
fauxAssistantMessage,
|
||||
type Model,
|
||||
type Provider,
|
||||
} from "@earendil-works/pi-ai";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { createHarness, type Harness } from "../harness.ts";
|
||||
|
||||
const INDIVIDUAL_BASE_URL = "https://api.individual.githubcopilot.com";
|
||||
const ENTERPRISE_BASE_URL = "https://api.enterprise.githubcopilot.com";
|
||||
|
||||
function seedCompactableSession(harness: Harness): void {
|
||||
harness.settingsManager.applyOverrides({ compaction: { keepRecentTokens: 1 } });
|
||||
const now = Date.now();
|
||||
harness.sessionManager.appendMessage({
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "message to compact" }],
|
||||
timestamp: now - 1000,
|
||||
});
|
||||
const model = harness.getModel();
|
||||
const assistant: AssistantMessage = {
|
||||
...fauxAssistantMessage("assistant response to compact", { timestamp: now - 500 }),
|
||||
api: model.api,
|
||||
provider: model.provider,
|
||||
model: model.id,
|
||||
usage: {
|
||||
input: 100,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
totalTokens: 100,
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
||||
},
|
||||
};
|
||||
harness.sessionManager.appendMessage(assistant);
|
||||
harness.session.agent.state.messages = harness.sessionManager.buildSessionContext().messages;
|
||||
}
|
||||
|
||||
describe("issue #6768 Copilot compaction base URL", () => {
|
||||
let harness: Harness | undefined;
|
||||
|
||||
afterEach(() => {
|
||||
harness?.cleanup();
|
||||
harness = undefined;
|
||||
});
|
||||
|
||||
it("uses the auth-resolved base URL through the SDK-style stream wrapper", async () => {
|
||||
harness = await createHarness();
|
||||
seedCompactableSession(harness);
|
||||
const catalogModel = { ...harness.getModel(), baseUrl: INDIVIDUAL_BASE_URL };
|
||||
harness.session.agent.state.model = catalogModel;
|
||||
|
||||
let requestBaseUrl: string | undefined;
|
||||
const respond = (requestModel: Model<string>) => {
|
||||
requestBaseUrl = requestModel.baseUrl;
|
||||
const stream = createAssistantMessageEventStream();
|
||||
stream.push({
|
||||
type: "done",
|
||||
reason: "stop",
|
||||
message: {
|
||||
...fauxAssistantMessage("summary"),
|
||||
api: requestModel.api,
|
||||
provider: requestModel.provider,
|
||||
model: requestModel.id,
|
||||
},
|
||||
});
|
||||
return stream;
|
||||
};
|
||||
const provider: Provider<string> = {
|
||||
id: catalogModel.provider,
|
||||
name: "Copilot regression provider",
|
||||
baseUrl: INDIVIDUAL_BASE_URL,
|
||||
auth: {
|
||||
apiKey: {
|
||||
name: "Copilot token",
|
||||
resolve: async ({ credential }) =>
|
||||
credential?.key ? { auth: { apiKey: credential.key }, source: "explicit token" } : undefined,
|
||||
},
|
||||
oauth: {
|
||||
name: "Copilot OAuth",
|
||||
login: async () => {
|
||||
throw new Error("unused");
|
||||
},
|
||||
refresh: async (credential) => credential,
|
||||
toAuth: async (credential) => ({
|
||||
apiKey: credential.access,
|
||||
baseUrl: ENTERPRISE_BASE_URL,
|
||||
}),
|
||||
},
|
||||
},
|
||||
getModels: () => [catalogModel],
|
||||
stream: (requestModel) => respond(requestModel),
|
||||
streamSimple: (requestModel) => respond(requestModel),
|
||||
};
|
||||
|
||||
await harness.authStorage.modify(catalogModel.provider, async () => ({
|
||||
type: "oauth",
|
||||
access: "enterprise-token",
|
||||
refresh: "refresh-token",
|
||||
expires: Date.now() + 60 * 60_000,
|
||||
}));
|
||||
const modelRuntime = harness.session.modelRuntime;
|
||||
modelRuntime.registerNativeProvider(provider);
|
||||
await modelRuntime.refresh({ allowNetwork: false, providers: [catalogModel.provider] });
|
||||
harness.session.agent.streamFunction = (model, context, options) =>
|
||||
modelRuntime.streamSimple(model, context, options);
|
||||
|
||||
await harness.session.compact();
|
||||
|
||||
expect(requestBaseUrl).toBe(ENTERPRISE_BASE_URL);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user