mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-21 01:01:42 +00:00
Fix API key priority and compaction bugs
- getEnvApiKey: ANTHROPIC_OAUTH_TOKEN now takes precedence over ANTHROPIC_API_KEY - findCutPoint: Stop scan-backwards loop at session header (was decrementing past it causing null preparation) - generateSummary/generateTurnPrefixSummary: Throw on stopReason=error instead of returning empty string - Test files: Fix API key priority order, use keepRecentTokens=1 for small test conversations
This commit is contained in:
parent
c58d5f20a4
commit
251fea752c
7 changed files with 98 additions and 110 deletions
|
|
@ -6359,6 +6359,23 @@ export const MODELS = {
|
||||||
contextWindow: 128000,
|
contextWindow: 128000,
|
||||||
maxTokens: 16384,
|
maxTokens: 16384,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
|
"meta-llama/llama-3.1-70b-instruct": {
|
||||||
|
id: "meta-llama/llama-3.1-70b-instruct",
|
||||||
|
name: "Meta: Llama 3.1 70B Instruct",
|
||||||
|
api: "openai-completions",
|
||||||
|
provider: "openrouter",
|
||||||
|
baseUrl: "https://openrouter.ai/api/v1",
|
||||||
|
reasoning: false,
|
||||||
|
input: ["text"],
|
||||||
|
cost: {
|
||||||
|
input: 0.39999999999999997,
|
||||||
|
output: 0.39999999999999997,
|
||||||
|
cacheRead: 0,
|
||||||
|
cacheWrite: 0,
|
||||||
|
},
|
||||||
|
contextWindow: 131072,
|
||||||
|
maxTokens: 4096,
|
||||||
|
} satisfies Model<"openai-completions">,
|
||||||
"meta-llama/llama-3.1-8b-instruct": {
|
"meta-llama/llama-3.1-8b-instruct": {
|
||||||
id: "meta-llama/llama-3.1-8b-instruct",
|
id: "meta-llama/llama-3.1-8b-instruct",
|
||||||
name: "Meta: Llama 3.1 8B Instruct",
|
name: "Meta: Llama 3.1 8B Instruct",
|
||||||
|
|
@ -6393,23 +6410,6 @@ export const MODELS = {
|
||||||
contextWindow: 10000,
|
contextWindow: 10000,
|
||||||
maxTokens: 4096,
|
maxTokens: 4096,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
"meta-llama/llama-3.1-70b-instruct": {
|
|
||||||
id: "meta-llama/llama-3.1-70b-instruct",
|
|
||||||
name: "Meta: Llama 3.1 70B Instruct",
|
|
||||||
api: "openai-completions",
|
|
||||||
provider: "openrouter",
|
|
||||||
baseUrl: "https://openrouter.ai/api/v1",
|
|
||||||
reasoning: false,
|
|
||||||
input: ["text"],
|
|
||||||
cost: {
|
|
||||||
input: 0.39999999999999997,
|
|
||||||
output: 0.39999999999999997,
|
|
||||||
cacheRead: 0,
|
|
||||||
cacheWrite: 0,
|
|
||||||
},
|
|
||||||
contextWindow: 131072,
|
|
||||||
maxTokens: 4096,
|
|
||||||
} satisfies Model<"openai-completions">,
|
|
||||||
"mistralai/mistral-nemo": {
|
"mistralai/mistral-nemo": {
|
||||||
id: "mistralai/mistral-nemo",
|
id: "mistralai/mistral-nemo",
|
||||||
name: "Mistral: Mistral Nemo",
|
name: "Mistral: Mistral Nemo",
|
||||||
|
|
@ -6546,23 +6546,6 @@ export const MODELS = {
|
||||||
contextWindow: 128000,
|
contextWindow: 128000,
|
||||||
maxTokens: 4096,
|
maxTokens: 4096,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
"openai/gpt-4o-2024-05-13": {
|
|
||||||
id: "openai/gpt-4o-2024-05-13",
|
|
||||||
name: "OpenAI: GPT-4o (2024-05-13)",
|
|
||||||
api: "openai-completions",
|
|
||||||
provider: "openrouter",
|
|
||||||
baseUrl: "https://openrouter.ai/api/v1",
|
|
||||||
reasoning: false,
|
|
||||||
input: ["text", "image"],
|
|
||||||
cost: {
|
|
||||||
input: 5,
|
|
||||||
output: 15,
|
|
||||||
cacheRead: 0,
|
|
||||||
cacheWrite: 0,
|
|
||||||
},
|
|
||||||
contextWindow: 128000,
|
|
||||||
maxTokens: 4096,
|
|
||||||
} satisfies Model<"openai-completions">,
|
|
||||||
"openai/gpt-4o": {
|
"openai/gpt-4o": {
|
||||||
id: "openai/gpt-4o",
|
id: "openai/gpt-4o",
|
||||||
name: "OpenAI: GPT-4o",
|
name: "OpenAI: GPT-4o",
|
||||||
|
|
@ -6597,6 +6580,23 @@ export const MODELS = {
|
||||||
contextWindow: 128000,
|
contextWindow: 128000,
|
||||||
maxTokens: 64000,
|
maxTokens: 64000,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
|
"openai/gpt-4o-2024-05-13": {
|
||||||
|
id: "openai/gpt-4o-2024-05-13",
|
||||||
|
name: "OpenAI: GPT-4o (2024-05-13)",
|
||||||
|
api: "openai-completions",
|
||||||
|
provider: "openrouter",
|
||||||
|
baseUrl: "https://openrouter.ai/api/v1",
|
||||||
|
reasoning: false,
|
||||||
|
input: ["text", "image"],
|
||||||
|
cost: {
|
||||||
|
input: 5,
|
||||||
|
output: 15,
|
||||||
|
cacheRead: 0,
|
||||||
|
cacheWrite: 0,
|
||||||
|
},
|
||||||
|
contextWindow: 128000,
|
||||||
|
maxTokens: 4096,
|
||||||
|
} satisfies Model<"openai-completions">,
|
||||||
"meta-llama/llama-3-70b-instruct": {
|
"meta-llama/llama-3-70b-instruct": {
|
||||||
id: "meta-llama/llama-3-70b-instruct",
|
id: "meta-llama/llama-3-70b-instruct",
|
||||||
name: "Meta: Llama 3 70B Instruct",
|
name: "Meta: Llama 3 70B Instruct",
|
||||||
|
|
@ -6716,23 +6716,6 @@ export const MODELS = {
|
||||||
contextWindow: 128000,
|
contextWindow: 128000,
|
||||||
maxTokens: 4096,
|
maxTokens: 4096,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
"openai/gpt-3.5-turbo-0613": {
|
|
||||||
id: "openai/gpt-3.5-turbo-0613",
|
|
||||||
name: "OpenAI: GPT-3.5 Turbo (older v0613)",
|
|
||||||
api: "openai-completions",
|
|
||||||
provider: "openrouter",
|
|
||||||
baseUrl: "https://openrouter.ai/api/v1",
|
|
||||||
reasoning: false,
|
|
||||||
input: ["text"],
|
|
||||||
cost: {
|
|
||||||
input: 1,
|
|
||||||
output: 2,
|
|
||||||
cacheRead: 0,
|
|
||||||
cacheWrite: 0,
|
|
||||||
},
|
|
||||||
contextWindow: 4095,
|
|
||||||
maxTokens: 4096,
|
|
||||||
} satisfies Model<"openai-completions">,
|
|
||||||
"openai/gpt-4-turbo-preview": {
|
"openai/gpt-4-turbo-preview": {
|
||||||
id: "openai/gpt-4-turbo-preview",
|
id: "openai/gpt-4-turbo-preview",
|
||||||
name: "OpenAI: GPT-4 Turbo Preview",
|
name: "OpenAI: GPT-4 Turbo Preview",
|
||||||
|
|
@ -6750,6 +6733,23 @@ export const MODELS = {
|
||||||
contextWindow: 128000,
|
contextWindow: 128000,
|
||||||
maxTokens: 4096,
|
maxTokens: 4096,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
|
"openai/gpt-3.5-turbo-0613": {
|
||||||
|
id: "openai/gpt-3.5-turbo-0613",
|
||||||
|
name: "OpenAI: GPT-3.5 Turbo (older v0613)",
|
||||||
|
api: "openai-completions",
|
||||||
|
provider: "openrouter",
|
||||||
|
baseUrl: "https://openrouter.ai/api/v1",
|
||||||
|
reasoning: false,
|
||||||
|
input: ["text"],
|
||||||
|
cost: {
|
||||||
|
input: 1,
|
||||||
|
output: 2,
|
||||||
|
cacheRead: 0,
|
||||||
|
cacheWrite: 0,
|
||||||
|
},
|
||||||
|
contextWindow: 4095,
|
||||||
|
maxTokens: 4096,
|
||||||
|
} satisfies Model<"openai-completions">,
|
||||||
"mistralai/mistral-tiny": {
|
"mistralai/mistral-tiny": {
|
||||||
id: "mistralai/mistral-tiny",
|
id: "mistralai/mistral-tiny",
|
||||||
name: "Mistral Tiny",
|
name: "Mistral Tiny",
|
||||||
|
|
|
||||||
|
|
@ -30,9 +30,13 @@ export function getEnvApiKey(provider: any): string | undefined {
|
||||||
return process.env.COPILOT_GITHUB_TOKEN || process.env.GH_TOKEN || process.env.GITHUB_TOKEN;
|
return process.env.COPILOT_GITHUB_TOKEN || process.env.GH_TOKEN || process.env.GITHUB_TOKEN;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ANTHROPIC_OAUTH_TOKEN takes precedence over ANTHROPIC_API_KEY
|
||||||
|
if (provider === "anthropic") {
|
||||||
|
return process.env.ANTHROPIC_OAUTH_TOKEN || process.env.ANTHROPIC_API_KEY;
|
||||||
|
}
|
||||||
|
|
||||||
const envMap: Record<string, string> = {
|
const envMap: Record<string, string> = {
|
||||||
openai: "OPENAI_API_KEY",
|
openai: "OPENAI_API_KEY",
|
||||||
anthropic: "ANTHROPIC_API_KEY",
|
|
||||||
google: "GEMINI_API_KEY",
|
google: "GEMINI_API_KEY",
|
||||||
groq: "GROQ_API_KEY",
|
groq: "GROQ_API_KEY",
|
||||||
cerebras: "CEREBRAS_API_KEY",
|
cerebras: "CEREBRAS_API_KEY",
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,15 @@ export default function (pi: HookAPI) {
|
||||||
|
|
||||||
ctx.ui.notify("Custom compaction hook triggered", "info");
|
ctx.ui.notify("Custom compaction hook triggered", "info");
|
||||||
|
|
||||||
const { messagesToSummarize, messagesToKeep, previousSummary, tokensBefore, resolveApiKey, entries, signal } =
|
const {
|
||||||
event;
|
messagesToSummarize,
|
||||||
|
messagesToKeep,
|
||||||
|
previousSummary,
|
||||||
|
tokensBefore,
|
||||||
|
resolveApiKey,
|
||||||
|
entries: _,
|
||||||
|
signal,
|
||||||
|
} = event;
|
||||||
|
|
||||||
// Use Gemini Flash for summarization (cheaper/faster than most conversation models)
|
// Use Gemini Flash for summarization (cheaper/faster than most conversation models)
|
||||||
const model = getModel("google", "gemini-2.5-flash");
|
const model = getModel("google", "gemini-2.5-flash");
|
||||||
|
|
|
||||||
|
|
@ -224,7 +224,7 @@ export function findCutPoint(
|
||||||
|
|
||||||
// Walk backwards from newest, accumulating estimated message sizes
|
// Walk backwards from newest, accumulating estimated message sizes
|
||||||
let accumulatedTokens = 0;
|
let accumulatedTokens = 0;
|
||||||
let cutIndex = startIndex; // Default: keep everything in range
|
let cutIndex = cutPoints[0]; // Default: keep from first message (not header)
|
||||||
|
|
||||||
for (let i = endIndex - 1; i >= startIndex; i--) {
|
for (let i = endIndex - 1; i >= startIndex; i--) {
|
||||||
const entry = entries[i];
|
const entry = entries[i];
|
||||||
|
|
@ -250,8 +250,8 @@ export function findCutPoint(
|
||||||
// Scan backwards from cutIndex to include any non-message entries (bash, settings, etc.)
|
// Scan backwards from cutIndex to include any non-message entries (bash, settings, etc.)
|
||||||
while (cutIndex > startIndex) {
|
while (cutIndex > startIndex) {
|
||||||
const prevEntry = entries[cutIndex - 1];
|
const prevEntry = entries[cutIndex - 1];
|
||||||
// Stop at compaction boundaries
|
// Stop at session header or compaction boundaries
|
||||||
if (prevEntry.type === "compaction") {
|
if (prevEntry.type === "session" || prevEntry.type === "compaction") {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (prevEntry.type === "message") {
|
if (prevEntry.type === "message") {
|
||||||
|
|
@ -320,6 +320,10 @@ export async function generateSummary(
|
||||||
|
|
||||||
const response = await complete(model, { messages: summarizationMessages }, { maxTokens, signal, apiKey });
|
const response = await complete(model, { messages: summarizationMessages }, { maxTokens, signal, apiKey });
|
||||||
|
|
||||||
|
if (response.stopReason === "error") {
|
||||||
|
throw new Error(`Summarization failed: ${response.errorMessage || "Unknown error"}`);
|
||||||
|
}
|
||||||
|
|
||||||
const textContent = response.content
|
const textContent = response.content
|
||||||
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
||||||
.map((c) => c.text)
|
.map((c) => c.text)
|
||||||
|
|
@ -550,6 +554,10 @@ async function generateTurnPrefixSummary(
|
||||||
|
|
||||||
const response = await complete(model, { messages: summarizationMessages }, { maxTokens, signal, apiKey });
|
const response = await complete(model, { messages: summarizationMessages }, { maxTokens, signal, apiKey });
|
||||||
|
|
||||||
|
if (response.stopReason === "error") {
|
||||||
|
throw new Error(`Turn prefix summarization failed: ${response.errorMessage || "Unknown error"}`);
|
||||||
|
}
|
||||||
|
|
||||||
return response.content
|
return response.content
|
||||||
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
||||||
.map((c) => c.text)
|
.map((c) => c.text)
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import { SessionManager } from "../src/core/session-manager.js";
|
||||||
import { SettingsManager } from "../src/core/settings-manager.js";
|
import { SettingsManager } from "../src/core/settings-manager.js";
|
||||||
import { codingTools } from "../src/core/tools/index.js";
|
import { codingTools } from "../src/core/tools/index.js";
|
||||||
|
|
||||||
const API_KEY = process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_OAUTH_TOKEN;
|
const API_KEY = process.env.ANTHROPIC_OAUTH_TOKEN || process.env.ANTHROPIC_API_KEY;
|
||||||
|
|
||||||
describe.skipIf(!API_KEY)("AgentSession branching", () => {
|
describe.skipIf(!API_KEY)("AgentSession branching", () => {
|
||||||
let session: AgentSession;
|
let session: AgentSession;
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import { SessionManager } from "../src/core/session-manager.js";
|
||||||
import { SettingsManager } from "../src/core/settings-manager.js";
|
import { SettingsManager } from "../src/core/settings-manager.js";
|
||||||
import { codingTools } from "../src/core/tools/index.js";
|
import { codingTools } from "../src/core/tools/index.js";
|
||||||
|
|
||||||
const API_KEY = process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_OAUTH_TOKEN;
|
const API_KEY = process.env.ANTHROPIC_OAUTH_TOKEN || process.env.ANTHROPIC_API_KEY;
|
||||||
|
|
||||||
describe.skipIf(!API_KEY)("AgentSession compaction e2e", () => {
|
describe.skipIf(!API_KEY)("AgentSession compaction e2e", () => {
|
||||||
let session: AgentSession;
|
let session: AgentSession;
|
||||||
|
|
@ -46,7 +46,7 @@ describe.skipIf(!API_KEY)("AgentSession compaction e2e", () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function createSession() {
|
function createSession(inMemory = false) {
|
||||||
const model = getModel("anthropic", "claude-sonnet-4-5")!;
|
const model = getModel("anthropic", "claude-sonnet-4-5")!;
|
||||||
|
|
||||||
const transport = new ProviderTransport({
|
const transport = new ProviderTransport({
|
||||||
|
|
@ -62,8 +62,10 @@ describe.skipIf(!API_KEY)("AgentSession compaction e2e", () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
sessionManager = SessionManager.create(tempDir);
|
sessionManager = inMemory ? SessionManager.inMemory() : SessionManager.create(tempDir);
|
||||||
const settingsManager = SettingsManager.create(tempDir, tempDir);
|
const settingsManager = SettingsManager.create(tempDir, tempDir);
|
||||||
|
// Use minimal keepRecentTokens so small test conversations have something to summarize
|
||||||
|
settingsManager.applyOverrides({ compaction: { keepRecentTokens: 1 } });
|
||||||
const authStorage = new AuthStorage(join(tempDir, "auth.json"));
|
const authStorage = new AuthStorage(join(tempDir, "auth.json"));
|
||||||
const modelRegistry = new ModelRegistry(authStorage);
|
const modelRegistry = new ModelRegistry(authStorage);
|
||||||
|
|
||||||
|
|
@ -156,64 +158,31 @@ describe.skipIf(!API_KEY)("AgentSession compaction e2e", () => {
|
||||||
expect(compaction.type).toBe("compaction");
|
expect(compaction.type).toBe("compaction");
|
||||||
if (compaction.type === "compaction") {
|
if (compaction.type === "compaction") {
|
||||||
expect(compaction.summary.length).toBeGreaterThan(0);
|
expect(compaction.summary.length).toBeGreaterThan(0);
|
||||||
// firstKeptEntryId can be 0 if all messages fit within keepRecentTokens
|
expect(typeof compaction.firstKeptEntryId).toBe("string");
|
||||||
// (which is the case for small conversations)
|
|
||||||
expect(compaction.firstKeptEntryId).toBeGreaterThanOrEqual(0);
|
|
||||||
expect(compaction.tokensBefore).toBeGreaterThan(0);
|
expect(compaction.tokensBefore).toBeGreaterThan(0);
|
||||||
}
|
}
|
||||||
}, 120000);
|
}, 120000);
|
||||||
|
|
||||||
it("should work with --no-session mode (in-memory only)", async () => {
|
it("should work with --no-session mode (in-memory only)", async () => {
|
||||||
const model = getModel("anthropic", "claude-sonnet-4-5")!;
|
createSession(true); // in-memory mode
|
||||||
|
|
||||||
const transport = new ProviderTransport({
|
|
||||||
getApiKey: () => API_KEY,
|
|
||||||
});
|
|
||||||
|
|
||||||
const agent = new Agent({
|
|
||||||
transport,
|
|
||||||
initialState: {
|
|
||||||
model,
|
|
||||||
systemPrompt: "You are a helpful assistant. Be concise.",
|
|
||||||
tools: codingTools,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create in-memory session manager
|
|
||||||
const noSessionManager = SessionManager.inMemory();
|
|
||||||
|
|
||||||
const settingsManager = SettingsManager.create(tempDir, tempDir);
|
|
||||||
const authStorage = new AuthStorage(join(tempDir, "auth.json"));
|
|
||||||
const modelRegistry = new ModelRegistry(authStorage);
|
|
||||||
|
|
||||||
const noSessionSession = new AgentSession({
|
|
||||||
agent,
|
|
||||||
sessionManager: noSessionManager,
|
|
||||||
settingsManager,
|
|
||||||
modelRegistry,
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Send prompts
|
// Send prompts
|
||||||
await noSessionSession.prompt("What is 2+2? Reply with just the number.");
|
await session.prompt("What is 2+2? Reply with just the number.");
|
||||||
await noSessionSession.agent.waitForIdle();
|
await session.agent.waitForIdle();
|
||||||
|
|
||||||
await noSessionSession.prompt("What is 3+3? Reply with just the number.");
|
await session.prompt("What is 3+3? Reply with just the number.");
|
||||||
await noSessionSession.agent.waitForIdle();
|
await session.agent.waitForIdle();
|
||||||
|
|
||||||
// Compact should work even without file persistence
|
// Compact should work even without file persistence
|
||||||
const result = await noSessionSession.compact();
|
const result = await session.compact();
|
||||||
|
|
||||||
expect(result.summary).toBeDefined();
|
expect(result.summary).toBeDefined();
|
||||||
expect(result.summary.length).toBeGreaterThan(0);
|
expect(result.summary.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
// In-memory entries should have the compaction
|
// In-memory entries should have the compaction
|
||||||
const entries = noSessionManager.getEntries();
|
const entries = sessionManager.getEntries();
|
||||||
const compactionEntries = entries.filter((e) => e.type === "compaction");
|
const compactionEntries = entries.filter((e) => e.type === "compaction");
|
||||||
expect(compactionEntries.length).toBe(1);
|
expect(compactionEntries.length).toBe(1);
|
||||||
} finally {
|
|
||||||
noSessionSession.dispose();
|
|
||||||
}
|
|
||||||
}, 120000);
|
}, 120000);
|
||||||
|
|
||||||
it("should emit correct events during auto-compaction", async () => {
|
it("should emit correct events during auto-compaction", async () => {
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import { SessionManager } from "../src/core/session-manager.js";
|
||||||
import { SettingsManager } from "../src/core/settings-manager.js";
|
import { SettingsManager } from "../src/core/settings-manager.js";
|
||||||
import { codingTools } from "../src/core/tools/index.js";
|
import { codingTools } from "../src/core/tools/index.js";
|
||||||
|
|
||||||
const API_KEY = process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_OAUTH_TOKEN;
|
const API_KEY = process.env.ANTHROPIC_OAUTH_TOKEN || process.env.ANTHROPIC_API_KEY;
|
||||||
|
|
||||||
describe.skipIf(!API_KEY)("Compaction hooks", () => {
|
describe.skipIf(!API_KEY)("Compaction hooks", () => {
|
||||||
let session: AgentSession;
|
let session: AgentSession;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue