feat(agent): Models is the harness's only auth path

Remove AgentHarnessOptions.getApiKeyAndHeaders: turn streaming,
compaction, and branch summarization resolve auth exclusively through
the injected Models instance. compact()/generateSummary()/
generateBranchSummary() lose their explicit apiKey/headers parameters.
This commit is contained in:
Mario Zechner
2026-06-10 21:39:13 +02:00
parent 10a575b76b
commit 9ab1292679
8 changed files with 30 additions and 146 deletions
+2 -35
View File
@@ -69,17 +69,6 @@ function cloneStreamOptions(streamOptions?: AgentHarnessStreamOptions): AgentHar
};
}
function mergeHeaders(...headers: Array<Record<string, string> | undefined>): Record<string, string> | undefined {
const merged: Record<string, string> = {};
let hasHeaders = false;
for (const entry of headers) {
if (!entry) continue;
Object.assign(merged, entry);
hasHeaders = true;
}
return hasHeaders ? merged : undefined;
}
function findDuplicateNames(names: string[]): string[] {
const seen = new Set<string>();
const duplicates = new Set<string>();
@@ -181,7 +170,6 @@ export class AgentHarness<
private thinkingLevel: ThinkingLevel;
private systemPrompt: AgentHarnessOptions<TSkill, TPromptTemplate, TTool>["systemPrompt"];
private streamOptions: AgentHarnessStreamOptions;
private getApiKeyAndHeaders?: AgentHarnessOptions["getApiKeyAndHeaders"];
private resources: AgentHarnessResources<TSkill, TPromptTemplate>;
private tools = new Map<string, TTool>();
private activeToolNames: string[];
@@ -199,7 +187,6 @@ export class AgentHarness<
this.resources = options.resources ?? {};
this.streamOptions = cloneStreamOptions(options.streamOptions);
this.systemPrompt = options.systemPrompt;
this.getApiKeyAndHeaders = options.getApiKeyAndHeaders;
this.validateUniqueNames(
(options.tools ?? []).map((tool) => tool.name),
"Duplicate tool name(s)",
@@ -372,11 +359,7 @@ export class AgentHarness<
private createStreamFn(getTurnState: () => AgentHarnessTurnState<TSkill, TPromptTemplate, TTool>): StreamFn {
return async (model, context, streamOptions) => {
const turnState = getTurnState();
const auth = await this.getApiKeyAndHeaders?.(model);
const snapshotOptions: AgentHarnessStreamOptions = {
...turnState.streamOptions,
headers: mergeHeaders(turnState.streamOptions.headers, auth?.headers),
};
const snapshotOptions: AgentHarnessStreamOptions = { ...turnState.streamOptions };
const requestOptions = await this.emitBeforeProviderRequest(model, turnState.sessionId, snapshotOptions);
return this.models.streamSimple(model, context, {
cacheRetention: requestOptions.cacheRetention,
@@ -397,7 +380,6 @@ export class AgentHarness<
sessionId: turnState.sessionId,
timeoutMs: requestOptions.timeoutMs,
transport: requestOptions.transport,
apiKey: auth?.apiKey,
});
};
}
@@ -709,8 +691,6 @@ export class AgentHarness<
try {
const model = this.model;
if (!model) throw new AgentHarnessError("invalid_state", "No model set for compaction");
// Explicit auth wins; otherwise the request resolves through provider auth.
const auth = await this.getApiKeyAndHeaders?.(model);
const branchEntries = await this.session.getBranch();
const preparationResult = prepareCompaction(branchEntries, DEFAULT_COMPACTION_SETTINGS);
if (!preparationResult.ok) throw preparationResult.error;
@@ -727,16 +707,7 @@ export class AgentHarness<
const provided = hookResult?.compaction;
const compactResult = provided
? { ok: true as const, value: provided }
: await compact(
preparation,
this.models,
model,
auth?.apiKey,
auth?.headers,
customInstructions,
undefined,
this.thinkingLevel,
);
: await compact(preparation, this.models, model, customInstructions, undefined, this.thinkingLevel);
if (!compactResult.ok) throw compactResult.error;
const result = compactResult.value;
const entryId = await this.session.appendCompaction(
@@ -789,13 +760,9 @@ export class AgentHarness<
if (!summaryText && options?.summarize && entries.length > 0) {
const model = this.model;
if (!model) throw new AgentHarnessError("invalid_state", "No model set for branch summary");
// Explicit auth wins; otherwise the request resolves through provider auth.
const auth = await this.getApiKeyAndHeaders?.(model);
const branchSummary = await generateBranchSummary(entries, {
models: this.models,
model,
apiKey: auth?.apiKey,
headers: auth?.headers,
signal: new AbortController().signal,
customInstructions: hookResult?.customInstructions ?? options?.customInstructions,
replaceInstructions: hookResult?.replaceInstructions ?? options?.replaceInstructions,
@@ -49,14 +49,10 @@ export interface CollectEntriesResult {
/** Options for generating a branch summary. */
export interface GenerateBranchSummaryOptions {
/** Provider collection the summarization request goes through. */
/** Provider collection the summarization request goes through; owns auth resolution. */
models: Models;
/** Model used for summarization. */
model: Model<any>;
/** Explicit API key; wins over provider-resolved auth. */
apiKey?: string;
/** Optional request headers forwarded to the provider. */
headers?: Record<string, string>;
/** Abort signal for the summarization request. */
signal: AbortSignal;
/** Optional instructions appended to or replacing the default prompt. */
@@ -204,16 +200,7 @@ export async function generateBranchSummary(
entries: SessionTreeEntry[],
options: GenerateBranchSummaryOptions,
): Promise<Result<BranchSummaryResult, BranchSummaryError>> {
const {
models,
model,
apiKey,
headers,
signal,
customInstructions,
replaceInstructions,
reserveTokens = 16384,
} = options;
const { models, model, signal, customInstructions, replaceInstructions, reserveTokens = 16384 } = options;
const contextWindow = model.contextWindow || 128000;
const tokenBudget = contextWindow - reserveTokens;
@@ -244,7 +231,7 @@ export async function generateBranchSummary(
const response = await models.completeSimple(
model,
{ systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages },
{ apiKey, headers, signal, maxTokens: 2048 },
{ signal, maxTokens: 2048 },
);
if (response.stopReason === "aborted") {
return err(new BranchSummaryError("aborted", response.errorMessage || "Branch summary aborted"));
@@ -457,8 +457,6 @@ export async function generateSummary(
models: Models,
model: Model<any>,
reserveTokens: number,
apiKey?: string,
headers?: Record<string, string>,
signal?: AbortSignal,
customInstructions?: string,
previousSummary?: string,
@@ -490,8 +488,8 @@ export async function generateSummary(
const completionOptions =
model.reasoning && thinkingLevel && thinkingLevel !== "off"
? { maxTokens, signal, apiKey, headers, reasoning: thinkingLevel }
: { maxTokens, signal, apiKey, headers };
? { maxTokens, signal, reasoning: thinkingLevel }
: { maxTokens, signal };
const response = await models.completeSimple(
model,
@@ -628,8 +626,6 @@ export async function compact(
preparation: CompactionPreparation,
models: Models,
model: Model<any>,
apiKey?: string,
headers?: Record<string, string>,
customInstructions?: string,
signal?: AbortSignal,
thinkingLevel?: ThinkingLevel,
@@ -659,24 +655,13 @@ export async function compact(
models,
model,
settings.reserveTokens,
apiKey,
headers,
signal,
customInstructions,
previousSummary,
thinkingLevel,
)
: Promise.resolve(ok<string, CompactionError>("No prior history.")),
generateTurnPrefixSummary(
turnPrefixMessages,
models,
model,
settings.reserveTokens,
apiKey,
headers,
signal,
thinkingLevel,
),
generateTurnPrefixSummary(turnPrefixMessages, models, model, settings.reserveTokens, signal, thinkingLevel),
]);
if (!historyResult.ok) return err(historyResult.error);
if (!turnPrefixResult.ok) return err(turnPrefixResult.error);
@@ -687,8 +672,6 @@ export async function compact(
models,
model,
settings.reserveTokens,
apiKey,
headers,
signal,
customInstructions,
previousSummary,
@@ -713,8 +696,6 @@ async function generateTurnPrefixSummary(
models: Models,
model: Model<any>,
reserveTokens: number,
apiKey?: string,
headers?: Record<string, string>,
signal?: AbortSignal,
thinkingLevel?: ThinkingLevel,
): Promise<Result<string, CompactionError>> {
@@ -737,8 +718,8 @@ async function generateTurnPrefixSummary(
model,
{ systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages },
model.reasoning && thinkingLevel && thinkingLevel !== "off"
? { maxTokens, signal, apiKey, headers, reasoning: thinkingLevel }
: { maxTokens, signal, apiKey, headers },
? { maxTokens, signal, reasoning: thinkingLevel }
: { maxTokens, signal },
);
if (response.stopReason === "aborted") {
return err(new CompactionError("aborted", response.errorMessage || "Turn prefix summarization aborted"));
+1 -4
View File
@@ -805,7 +805,7 @@ export interface AgentHarnessOptions<
/**
* Provider collection used for all model requests (turn streaming,
* compaction, branch summarization). Auth resolves through the providers'
* auth; explicit per-request values (`getApiKeyAndHeaders`) win per field.
* auth.
*/
models: Models;
tools?: TTool[];
@@ -824,9 +824,6 @@ export interface AgentHarnessOptions<
activeTools: TTool[];
resources: AgentHarnessResources<TSkill, TPromptTemplate>;
}) => string | Promise<string>);
getApiKeyAndHeaders?: (
model: Model<any>,
) => Promise<{ apiKey: string; headers?: Record<string, string> } | undefined>;
/** Curated stream/provider request options. Snapshotted at turn start. */
streamOptions?: AgentHarnessStreamOptions;
model: Model<any>;